Skip to content

Conversation

@AndlerRL
Copy link
Member

@AndlerRL AndlerRL commented Jan 13, 2026

Summary by CodeRabbit

  • New Features

    • Added scoped retrieval-augmented generation to improve chat context with similarity-based answer matching.
    • Introduced document thread outline rendering with scope-based message organization.
    • Added prompt enhancement UI with model-specific structured input fields and enhancement options.
    • Implemented scope-aware document editing for heading-level and document-root operations.
    • Added document operation tracking to log and monitor document edits.
  • Improvements

    • Enhanced conversation windowing to provide better scoped context retrieval.
    • Improved search functionality for assistant answers using vector similarity.

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

@coderabbitai coderabbitai bot changed the title Vibe/threadrefactor | @coderabbitai Vibe/threadrefactor | Add vector RAG and document outline rendering Jan 13, 2026
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: 16

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/components/routes/chat/prompt-form/index.tsx (1)

96-105: SSR/CSR mismatch risk: new Date() at module level.

The change to a constant ID is good for SSR stability. However, expires: new Date().toISOString() at line 103 will still produce different values between server and client renders, potentially causing hydration warnings.

Consider initializing expires lazily or using a placeholder value since this default is immediately overwritten in triggerNewTextFileDialog (line 287) anyway.

Suggested fix
 const DEFAULT_FILE_ATTACHMENT: FileAttachment = {
 	id: 'thread-context-default',
 	name: 'Thread Context.txt',
 	url: DEFAULT_TEXT_FILE_BASE64_CONTENT,
 	contentType: 'text/plain',
 	content: DEFAULT_TEXT_FILE_BASE64_CONTENT,
 	messageIds: [],
-	expires: new Date().toISOString(),
+	expires: '', // Set at runtime in triggerNewTextFileDialog
 	size: DEFAULT_TEXT_FILE_CONTENT.length,
 }
🤖 Fix all issues with AI agents
In @apps/hasura/migrations/masterbots/1766624501677_assistant_answers/down.sql:
- Around line 1-2: The leading comment in the migration is misleading—remove or
update the comment that says "drop index first (name it explicitly in up.sql so
this is stable)" because the migration only runs "DROP TABLE IF EXISTS
public.assistant_answers;" and PostgreSQL automatically removes indexes with the
table; either delete the comment or replace it with an accurate note (e.g.,
"indexes are dropped with the table; explicit index drops not required") so
future maintainers aren’t confused; target the comment immediately above the
DROP TABLE statement in this migration.

In
@apps/hasura/migrations/masterbots/1766957388317__op_meta_and_scoped_rag/up.sql:
- Around line 92-94: The WHERE clause comparing aa.scope_id = p_scope_id
incorrectly excludes rows when both are NULL; update the condition to use
NULL-safe comparison (aa.scope_id IS NOT DISTINCT FROM p_scope_id) so rows with
both NULL scope_id (used for the doc_root scope_type) are preserved; apply this
change in the query that includes the predicates aa.thread_id = p_thread_id AND
aa.scope_type = p_scope_type AND aa.scope_id = p_scope_id.

In
@apps/hasura/migrations/masterbots/1767498310895_add_public_message_docid/up.sql:
- Around line 38-43: Remove the garbled comment artifact
"contentReference[oaicite:0]{index=0}" and guard the two index creations
(assistant_answers_thread_doc_scope_idx and
assistant_answers_thread_doc_scope_turn_idx) with a defensive check that the
assistant_answers table actually has both scope_type and scope_id columns;
implement this by wrapping the CREATE INDEX statements in a conditional DO block
or IF EXISTS query that checks information_schema.columns (or pg_catalog) for
both column names on table assistant_answers and only runs the CREATE INDEX ...
statements when both columns are present.

In @apps/pro-web/app/actions/chat-memory/embedding.ts:
- Around line 10-34: The embedText1536 function lacks the input validation used
in the sibling implementation, so add trimming and empty-string handling at the
top of embedText1536: derive text = (input ?? '').trim() and if text is empty
return [] before fetching the OpenAI embeddings; this prevents calling the API
for empty/whitespace inputs and keeps the returned type consistent.

In @apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts:
- Around line 155-173: The catch block currently swallows the original error
when it isn't a schema-lag case; change it to preserve and surface that error:
capture the caught error in the catch (error) block, only attempt the retry
branch when docNodeId && error.message indicates schema issues (as currently
done for doc_node_id/field/assistant_answers_insert_input), but if the message
does not match those conditions rethrow the original error (or at minimum log
it) instead of returning; additionally, if the retry path is taken and the retry
fails, rethrow or attach both the retry error and the original error so callers
see both contexts (refer to docNodeId, baseObject, hasuraGql, and insert to
locate and update the relevant logic).

In @apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts:
- Around line 365-372: The normalize function is using an unsafe double-cast
("as unknown as AssistantAnswerMemory[]") which hides type mismatch between
SearchRow and AssistantAnswerMemory; remove the cast and return a properly typed
array instead (update normalize to have a return type of AssistantAnswerMemory[]
and let the mapped object conform to that type), ensuring the mapped properties
message_id, turn_index, content, cosine, and created_at are renamed/typed to
messageId, turnIndex, content, cosine, and createdAt to match
AssistantAnswerMemory.
- Around line 81-84: The estimateTokens function returns 0 for empty strings
which is inconsistent with the rest of the codebase; update estimateTokens to
never return 0 by clamping the result to a minimum of 1 (i.e., use Math.max(1,
Math.ceil((text ?? '').length / 4))) or, better, import and reuse the shared
token-estimation implementation used elsewhere to ensure consistency with
existing token budget logic; locate the function named estimateTokens in
retrieve-assistant-answer-memories.ts and replace its body or import the
canonical estimator used in simple-rag/prompt.ts.
- Around line 123-134: tryExtractExpectedType builds a RegExp by interpolating
varName directly which can introduce a ReDoS risk if varName contains regex
meta-characters; escape/sanitize varName before using it in the pattern (e.g.,
implement and use an escapeRegExp that backslashes regex metacharacters, or
otherwise construct the pattern without direct interpolation), then use the
escaped value when creating the RegExp in tryExtractExpectedType so the pattern
is safe and deterministic.

In @apps/pro-web/components/routes/chat/chat-list/index.tsx:
- Around line 109-114: The double type assertion on chatMessages and
previousChatMessages hides type errors; create a normalizeMessages(raw:
unknown[] | undefined): ThreadMessage[] helper that validates and maps each raw
item into a ThreadMessage (perform runtime checks for required keys like
messageId/id, content, role and provide sane defaults or skip invalid items),
replace the direct casts in the component to call normalizeMessages(messages)
and normalizeMessages(activeThread?.thread?.messages), and update the messages
prop usage so the component receives/handles unknown[] safely instead of using
"as unknown as ThreadMessage[]".
- Around line 117-118: The expression using "as unknown as any" to derive
threadAttachments is unsafe; instead ensure activeThread has a proper typed
interface (e.g., a ChatThread type with metadata?: { attachments?:
FileAttachment[] }) or perform a runtime type guard before casting: narrow
activeThread to the correct shape (or implement an isFileAttachmentArray(value):
value is FileAttachment[] check that validates Array.isArray and expected
fields) and then read metadata?.attachments as FileAttachment[] only after the
guard. Update the threadAttachments declaration to use the narrowed type or
guard so you avoid the double any cast and preserve type safety.

In
@apps/pro-web/components/routes/workspace/document-tab/document-thread-outline.tsx:
- Around line 113-117: The component returns early before calling the React hook
useThread(), violating the Rules of Hooks; move the call to useThread() (and any
other hooks like useMemo for validScopeIds) to the top of the component so hooks
are invoked unconditionally, then perform the conditional early return based on
outline (i.e., call useThread() first, then if (!outline?.length) return null,
and compute validScopeIds via useMemo(buildValidScopeIdSet, [outline]) after
hooks are declared).

In
@apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx:
- Around line 244-278: The debounced save timer stored in sectionSaveTimerRef
(used by scheduleSectionSave) isn't cleared on unmount; add a useEffect that
returns a cleanup function which checks sectionSaveTimerRef.current, calls
clearTimeout on it and sets it to null so no pending timeout fires after the
component unmounts (ensure the effect has no deps or uses refs only so it runs
once).
- Around line 304-324: The Sections/Source buttons in
WorkspaceDocumentTextEditor read viewMode for styling but never change it; pass
the parent's handleViewSourceToggle prop into WorkspaceDocumentTextEditor and
call handleViewSourceToggle('sections') for the Sections button and
handleViewSourceToggle('source') for the Source button (you can still call
setIsPreview(false) as before). Update the component signature to accept
handleViewSourceToggle and replace the onClick handlers that currently only call
setIsPreview with calls that invoke handleViewSourceToggle with the correct
mode.

In @apps/pro-web/components/shared/shared-accordion.tsx:
- Line 374: The current assignment to the local variable "category" can throw
when chatbot.categories is an empty array; change the access to safely handle
empty arrays by using optional chaining on the index (e.g.,
thread?.chatbot?.categories?.[0]?.category?.name) or guard with a length check
(e.g., const categories = thread?.chatbot?.categories; const category =
categories && categories.length ? categories[0].category?.name : undefined) so
that accessing categories[0] never throws; update the assignment where
"category" is declared.

In @apps/pro-web/lib/hooks/use-workspace-chat.tsx:
- Around line 643-645: The template literal assigned to newContentMarkdown
includes unintended tab characters and indentation around the newline, which
will embed tabs into the document content; update the assembly of
newContentMarkdown (using preservedBeforeSelectionRef.current, cleanedResponse,
preservedAfterSelectionRef.current) so there are no leading tabs or indentation
in the template—either remove the tabs in the backtick literal or build the
string with explicit newlines (e.g., concatenate with "\n\n") or trim the pieces
before joining to ensure clean, unindented content.
- Around line 745-746: The fix: stop calling useWorkspace() and useThread()
inside the async handler handleWorkspaceEdit; instead use the instances already
obtained at the top-level of the provider and add the missing methods to that
destructure. Update the top-level destructure of useWorkspace() to include
toggleWorkspace and the top-level destructure of useThread() to include
setIsOpenPopup (or their exact internal names), then replace the in-handler
calls in handleWorkspaceEdit with those top-level variables so no hooks are
invoked inside the event handler.
🧹 Nitpick comments (46)
packages/mb-env/src/config.env.ts (2)

46-56: Consider adding NaN fallback protection for numeric parsing.

If environment variables are set to non-numeric values (e.g., NEXT_PUBLIC_MB_SIMPLE_RAG_K="abc"), Number.parseInt and Number.parseFloat return NaN instead of the intended default. This could cause unexpected behavior downstream.

While this follows the existing pattern in the file (e.g., experimentalStreamDelayMs, maxAttachments), you may want to add fallback protection for these new RAG parameters since they directly affect retrieval quality.

♻️ Optional: Add NaN fallback protection
-		simpleRagK: Number.parseInt(
-			process.env.NEXT_PUBLIC_MB_SIMPLE_RAG_K || '3',
-			10,
-		),
-		simpleRagMinCosine: Number.parseFloat(
-			process.env.NEXT_PUBLIC_MB_SIMPLE_RAG_MIN_COSINE || '0.60',
-		),
-		simpleRagTokenBudget: Number.parseInt(
-			process.env.NEXT_PUBLIC_MB_SIMPLE_RAG_TOKEN_BUDGET || '600',
-			10,
-		),
+		simpleRagK:
+			Number.parseInt(process.env.NEXT_PUBLIC_MB_SIMPLE_RAG_K || '3', 10) || 3,
+		simpleRagMinCosine:
+			Number.parseFloat(process.env.NEXT_PUBLIC_MB_SIMPLE_RAG_MIN_COSINE || '0.60') || 0.6,
+		simpleRagTokenBudget:
+			Number.parseInt(process.env.NEXT_PUBLIC_MB_SIMPLE_RAG_TOKEN_BUDGET || '600', 10) || 600,

58-59: Clean up internal tracking comment.

The (Commit 10) reference in the comment appears to be an internal development tracking artifact. Consider removing it for cleaner documentation.

🧹 Suggested cleanup
-		// Chat observability (Commit 10)
+		// Chat observability
apps/pro-web/lib/markdown-utils-commit10testarchive.ts (2)

1-6: Non-standard test file naming.

The file name markdown-utils-commit10testarchive.ts doesn't follow the *.test.ts convention. This may prevent test runners from discovering these tests automatically. The "commit10testarchive" suffix also suggests this might be temporary/archive code.

Consider renaming to markdown-utils.test.ts or removing if this is archived code that shouldn't be committed. Based on learnings, test files should follow the *.test.ts or *.spec.ts pattern.


15-49: Test logic is sound.

The test follows the AAA pattern correctly and the null guard on line 39 is good defensive practice. The assertions comprehensively verify subtree boundaries.

Minor: The type assertion as (typeof sections)[number] on line 35 is redundant since byTitle already returns the matching element type.

Optional: Remove redundant type assertion
-		const a = byTitle(sections, 'A') as (typeof sections)[number]
+		const a = byTitle(sections, 'A')
vitest.config.ts (1)

4-13: Consider whether to integrate vite-tsconfig-paths with root tsconfig configuration.

The vite-tsconfig-paths package is present in devDependencies but not used. However, leveraging it would require adding path aliases to the root tsconfig.json. Currently, the manual alias approach is straightforward and doesn't require additional tsconfig changes. If adopting vite-tsconfig-paths, ensure the root tsconfig.json includes the @ path alias to keep configuration centralized and avoid duplication.

apps/pro-web/lib/queries/use-workspace-structure.ts (1)

347-359: Consider updating the WorkspaceDocumentMetadata type to include docNodeId.

The as any casts on lines 352 and 359 bypass type checking. Since docNodeId is now a computed property on documents, adding it to the type definition would improve type safety and editor support.

♻️ Suggested type update

In @/types/thread.types (or wherever WorkspaceDocumentMetadata is defined):

 export interface WorkspaceDocumentMetadata {
   id: string
   name: string
   project: string
   // ... other fields
+  docNodeId?: string | null
 }

Then remove the as any casts:

 finalStructure.documents = Array.from(mergedDocuments.values()).map(
   (doc) => ({
     ...doc,
     docNodeId:
-      (doc as any).docNodeId ??
+      doc.docNodeId ??
       getCanonicalDocNodeId({
         docId: doc.id,
         project: doc.project,
         documentName: doc.name,
       }),
   }),
-)  as any
+)
apps/pro-web/components/routes/workspace/document-tab/workspace-document-section-tree.tsx (1)

57-63: Inline function in map callback creates new function per render.

The handleDelete adapter is defined inside the map callback, creating a new function instance for each node on every render. While this works correctly, it could cause unnecessary re-renders of WorkspaceDocumentSectionItem if it uses React.memo.

For a non-blocking improvement, consider extracting the adapter logic or using useCallback if performance becomes a concern. The current implementation is functionally correct.

apps/pro-web/components/routes/chat/chat-list/message-pairs.tsx (1)

186-208: Consider simplifying the no-op outline branch.

Both shouldRenderOutline branches return the same flat JSX. While the comment explains this is intentional scaffolding, the conditional structure is misleading. Consider simplifying:

♻️ Suggested simplification
-	// ✅ Branch gate: outline rendering only when explicitly enabled AND we have a tree.
-	// Non-doc threads never pass outlineTree, so they always use flat rendering.
-	const shouldRenderOutline = Boolean(
-		enableOutlineRendering && outlineTree?.length,
-	)
-
-	const flat = (
+	// TODO: When outline rendering is implemented, use `enableOutlineRendering && outlineTree?.length`
+	// to branch between flat and outline rendering. For now, always use flat rendering.
+	return (
 		<>
 			{previousPairsElements}
 			{showSeparator && (
 				<Separator className="relative my-6 h-0.5 z-[2] rounded-sm bg-iron dark:bg-mirage" />
 			)}
 			{currentPairsElements}
 		</>
 	)
-
-	// For now, outline path intentionally uses flat rendering to avoid regressions.
-	// Next step: replace this with recursive outline rendering rules (children accordions first, then node messages).
-	if (shouldRenderOutline) {
-		return flat
-	}
-
-	return flat
 }
apps/web/components/shared/shared-accordion.tsx (3)

98-99: Consider removing unused destructured variable.

The searchTerm is destructured but marked as unused with _searchTerm. If it's not needed now, consider removing it entirely to reduce noise. Biome may also flag this.

♻️ Suggested change
-	// currently unused but kept to avoid behavior regressions (future: highlight matches)
-	const { searchTerm: _searchTerm } = useThreadSearch()
+	// Note: useThreadSearch().searchTerm available for future highlight matches feature
+	useThreadSearch()

248-260: Guard against missing threadSlug is good, but return value is confusing.

The guard correctly prevents navigation with invalid data. However, returning thread (the partial prop) when slug is missing is semantically confusing since updateActiveThread typically returns the thread on success. Consider returning undefined or null for consistency with error cases.

♻️ Suggested change
 		// Guard: avoid url builder errors + accidental navigation to "/"
 		if (!resolvedThreadSlug) {
 			if (appConfig.features.devMode) {
 				console.error('updateActiveThread: missing threadSlug for navigation', {
 					threadId: thread.threadId,
 					fullThreadSlug: ft?.slug,
 					fullThreadThreadSlug: ft?.thread?.slug,
 					threadPropSlug: asThreadLike(thread)?.slug,
 					paramsThreadSlug: params.threadSlug,
 				})
 			}
-			return thread
+			return undefined
 		}

336-346: Duplicate slug resolution and validation pattern.

Lines 336-346 and 380-390 contain nearly identical slug resolution and validation logic. Consider extracting to a helper function to reduce duplication.

♻️ Optional: Extract shared validation logic
const validateAndGetSlug = (
  thread: SharedAccordionProps['thread'],
  context: string,
): string | null => {
  const resolvedSlug = asThreadLike(thread)?.slug ?? asString(params.threadSlug)
  if (!resolvedSlug && appConfig.features.devMode) {
    console.error(`Missing required parameters for ${context}: threadSlug`)
  }
  return resolvedSlug ?? null
}

Also applies to: 380-390

apps/pro-web/components/routes/chat/chat-list/message-pair-accordion.tsx (2)

40-44: Type assertion in asThreadLike bypasses type safety.

The asThreadLike helper uses as ThreadLike which doesn't validate the object's shape at runtime. If the object has unexpected types (e.g., slug is a number), the code will fail silently downstream.

Consider adding minimal runtime validation for critical fields if this data comes from untrusted sources, or document that the caller is responsible for ensuring shape conformance.


217-221: Dead code paths due to shouldShowUserMessage = true.

With shouldShowUserMessage hardcoded to true, the following code is unreachable:

  • Lines 246-251: CSS classes for !shouldShowUserMessage cases
  • Lines 269-292: The first branch of AnimatePresence (tooltip for continued threads)

Consider removing these dead code paths to improve maintainability, or add a comment explaining if this is intentional scaffolding for future changes.

♻️ Suggested cleanup for dead code
-			// Safety path (kept, but should never hit with shouldShowUserMessage=true)
-			'bg-transparent dark:bg-transparent w-auto h-auto px-4 py-0 border-none ml-auto':
-				!shouldShowUserMessage,
-			'w-full justify-between':
-				!shouldShowUserMessage && isPrevious && !isAccordionFocused,

And consider simplifying the AnimatePresence section since the first branch never renders.

Also applies to: 246-251, 269-292

apps/pro-web/lib/hooks/__test__/simple-rag-prompt.test.ts (1)

7-96: Consider adding edge case tests.

The test suite covers the main functionality well. Consider adding tests for:

  • Empty userQuestionChain and retrievedAnswers
  • All items below minCosine threshold (should return empty retrieval block)
  • Items with empty/whitespace-only content (should be filtered)

These are optional but would improve robustness.

apps/pro-web/lib/select-scope-conversation-window.ts (2)

232-235: Silent error handling loses diagnostic context.

The catch block returns an empty result without logging. While fail-open is appropriate for prompt assembly, consider adding a warning log for observability:

♻️ Add warning log for error visibility
-	} catch {
-		// Fail-open: never block prompt assembly.
-		return { userQuestions: [], lastAssistant: null, lastUser: null }
+	} catch (error) {
+		// Fail-open: never block prompt assembly, but log for observability.
+		console.warn('[selectScopeConversationWindow] Query failed, returning empty window:', error)
+		return { userQuestions: [], lastAssistant: null, lastUser: null }
	}

192-193: Type casts bypass mb-genql type checking on dynamically constructed where clauses.

The as unknown as never pattern on lines 192, 205, and 219 suppresses TypeScript errors for runtime-built where objects. While this works and is more type-safe than plain any (as used in hasura.service.ts), it sacrifices the type-safety benefits mb-genql provides. Consider creating typed where clause builders or a generic where type if mb-genql's constraints allow, but the current approach is acceptable given the dynamic nature of scope building.

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

143-146: Theoretical infinite loop risk if ID space is exhausted.

While extremely unlikely in practice (36^10 possible IDs), the do-while loop has no upper bound. Consider adding a safety counter for defensive coding.

💡 Optional: Add safety bound
 		if (!finalId) {
-			// Insert a new marker line
+			// Insert a new marker line (safety bound to avoid infinite loop)
+			let attempts = 0
 			do {
 				finalId = generateShortSectionId()
-			} while (used.has(finalId))
+				attempts++
+			} while (used.has(finalId) && attempts < 1000)
 			used.add(finalId)
apps/pro-web/components/routes/thread/thread-popup.tsx (1)

128-131: Unsafe type assertions bypass type checking.

Using (threadMeta as any) loses type safety. Consider defining a proper interface for thread metadata or using a type guard.

💡 Suggested fix: Define metadata interface
+interface ThreadMetadata {
+	docNodeId?: string
+	doc_node_id?: string
+	documentMarkdown?: string
+	markdown?: string
+	docMarkdown?: string
+	content?: string
+	documents?: Array<{ content?: string; markdown?: string; docMarkdown?: string }>
+}
+
+function isThreadMetadata(meta: unknown): meta is ThreadMetadata {
+	return meta !== null && typeof meta === 'object'
+}
+
 const threadMeta = (currentThread?.metadata as unknown) ?? null
-const docNodeId = pickString(
-	(threadMeta as any)?.docNodeId,
-	(threadMeta as any)?.doc_node_id,
-)
+const typedMeta = isThreadMetadata(threadMeta) ? threadMeta : null
+const docNodeId = pickString(
+	typedMeta?.docNodeId,
+	typedMeta?.doc_node_id,
+)
apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx (2)

509-525: Props definition should use interface per coding guidelines.

As per coding guidelines, "Use interface for objects/classes; type for unions/tuples/aliases". Since this is a props object, it should be an interface.

💡 Suggested fix
-export type WorkspaceTextEditorProps = {
+export interface WorkspaceTextEditorProps {
 	activeSection: string | null
 	setActiveSection: (sectionId: string | null) => void
 	viewMode: 'sections' | 'source'
 
 	sectionTextareaRef: React.RefObject<HTMLTextAreaElement>
 	sourceTextareaRef: React.RefObject<HTMLTextAreaElement>
 
 	handleCursorPositionChange: (e?: React.SyntheticEvent) => void
 
 	handleSectionClick: (sectionId: string) => void
 	markUserTyping: () => void
 
 	handleExpandSection: (sectionTitle: string) => void
 	handleRewriteSection: (sectionTitle: string) => void
 }

505-507: Consider extracting clamp to a shared utility file if planning to reuse across the codebase.

Currently, a generic clamp utility only exists in this file. While it's a common utility pattern, extraction to apps/pro-web/lib/utils.ts would be beneficial for future reuse, though not immediately necessary given the single usage.

apps/pro-web/app/actions/chat-memory/embed-text-1536.ts (2)

17-29: Consider adding a timeout for the OpenAI API call.

The fetch call has no timeout configured. OpenAI API calls can occasionally hang, which could cause the server action to block indefinitely.

♻️ Suggested fix with AbortController timeout
+	const controller = new AbortController()
+	const timeoutId = setTimeout(() => controller.abort(), 30_000)
+
 	const res = await fetch('https://api.openai.com/v1/embeddings', {
 		method: 'POST',
 		headers: {
 			'content-type': 'application/json',
 			authorization: `Bearer ${apiKey}`,
 		},
 		body: JSON.stringify({
 			model: EMBEDDING_MODEL,
 			input: text,
 			encoding_format: 'float',
 		}),
 		cache: 'no-store',
+		signal: controller.signal,
 	})
+
+	clearTimeout(timeoutId)

38-40: Consider validating the embedding dimension.

The function name embedText1536 implies a 1536-dimension vector, but there's no validation that the returned embedding matches this expectation. If OpenAI changes the model output or the EMBEDDING_MODEL constant is updated, this could silently return unexpected dimensions.

♻️ Optional dimension validation
 	const json = (await res.json()) as OpenAIEmbeddingsResponse
 	const vec = json.data?.[0]?.embedding ?? []
+	if (vec.length > 0 && vec.length !== 1536) {
+		console.warn(`Unexpected embedding dimension: ${vec.length}, expected 1536`)
+	}
 	return vec
apps/pro-web/services/hasura/simple-rag.ts (1)

7-25: Add explicit return type annotation.

Per coding guidelines, function return types should be explicitly annotated. The function implicitly returns Promise<void>.

♻️ Add return type
-export async function ingestAssistantAnswer(input: IngestAssistantAnswerInput) {
+export async function ingestAssistantAnswer(input: IngestAssistantAnswerInput): Promise<void> {
apps/pro-web/app/actions/chat-memory/embedding.ts (1)

1-43: Consolidate embedding utilities to avoid dual implementations with diverging quality.

Two implementations of embedText1536 exist with different features: the utility version in embedding.ts lacks input validation and enhanced error handling, while the server action in embed-text-1536.ts includes these improvements. This divergence creates maintenance burden and the risk of using the weaker implementation.

Recommend making embed-text-1536.ts the canonical implementation (it's already marked 'use server' and includes validation) and have embedding.ts re-export from it for utilities, or move the enhanced logic directly into embedding.ts if both client and server paths need it.

apps/pro-web/types/operation-meta.ts (2)

13-31: Consider making derived fields optional if they may not always be present.

The comment indicates these are "Derived fields (no classifier)" - if these fields are computed and may not always be available during construction, consider making them optional:

 // Derived fields (no classifier)
-intent_source: IntentSource
-exclude_from_q_chain: boolean
-rag_eligible: boolean
+intent_source?: IntentSource
+exclude_from_q_chain?: boolean
+rag_eligible?: boolean

If they're always guaranteed to be present, the current definition is appropriate.


5-5: Centralize ScopeType to prevent duplication and drift.

ScopeType is duplicated across four files:

  • apps/pro-web/types/operation-meta.ts (line 5) — already in the correct central location
  • apps/pro-web/lib/select-scope-conversation-window.ts (line 4)
  • apps/pro-web/services/hasura/hasura.service.ts (line 782)
  • apps/pro-web/components/routes/pro/chat-panel-pro.tsx (line 55)

Export ScopeType from operation-meta.ts and import it in the other files to maintain a single source of truth and align with the guideline of placing shared TypeScript definitions under types/.

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

782-782: Import ScopeType from centralized types instead of redefining.

This duplicates the ScopeType definition from apps/pro-web/types/operation-meta.ts. Consider importing it:

+import type { ScopeType } from '@/types/operation-meta'
-type ScopeType = 'chat_thread' | 'doc_root' | 'heading_thread'
apps/hasura/migrations/masterbots/1766629067789_assistant_answers_fn/up.sql (1)

35-55: GraphQL wrapper is appropriate; consider input validation.

The text-to-vector cast at Line 50 will fail at runtime if p_query_vec_text isn't a valid 1536-dimensional vector string. This is acceptable for internal use but could benefit from a guard if exposed to untrusted input.

Optional: Add dimension validation
-- If defensive validation is desired:
CREATE OR REPLACE FUNCTION public.search_assistant_answers_gql(...)
AS $$
  SELECT *
  FROM public.search_assistant_answers(
    p_thread_id,
    CASE 
      WHEN array_length(string_to_array(trim(both '[]' from p_query_vec_text), ','), 1) = 1536 
      THEN p_query_vec_text::vector(1536)
      ELSE NULL
    END,
    ...
  );
$$;
apps/pro-web/components/routes/workspace/document-tab/section-subaccordion-row.tsx (1)

42-89: Component structure is clean; consider extracting onClick handler.

The component logic is well-organized with proper memoization and defensive checks. However, the inline arrow function at Line 81 could be extracted per coding guidelines.

As per coding guidelines, prefer stable function references for event handlers:

Extract onClick handler
 export function SectionSubaccordionRow({
 	nodes,
 	maxPreview = 3,
 	renderNode,
 	className,
 }: SectionSubaccordionRowProps) {
 	const [expanded, setExpanded] = useState(false)
+
+	function handleExpand() {
+		setExpanded(true)
+	}

 	// ... rest of component

 				{hasOverflow && !expanded ? (
 					<Button
 						type="button"
 						variant="ghost"
 						size="sm"
 						className="h-7 px-2 text-xs"
-						onClick={() => setExpanded(true)}
+						onClick={handleExpand}
 					>
 						See more…
 					</Button>
 				) : null}
apps/pro-web/components/routes/pro/chat-panel-pro.tsx (3)

275-281: Multiple as any casts indicate missing type definitions.

The chained fallbacks with as any casts suggest currentDocumentData lacks proper type definitions for docNodeId and id fields. This should be addressed in the query types.

Add proper typing to document data

Consider extending the useDocumentContent return type to include docNodeId and id:

// In the query types file
interface DocumentContentData {
  content: string
  sections?: MarkdownSection[]
  docNodeId?: string
  id?: string
}

345-361: IIFE pattern works but could be simplified.

The immediately-invoked function expression for scope computation works correctly but adds cognitive overhead. A simpler conditional assignment would be clearer.

Simplify scope computation
-		const scope =
-			scopeType !== 'chat_thread'
-				? (() => {
-						const fullMarkdown = currentDocumentData?.content ?? ''
-						const sections =
-							currentDocumentData?.sections ??
-							(fullMarkdown ? parseMarkdownSections(fullMarkdown) : undefined)
-
-						return {
-							scopeType,
-							scopeId,
-							docNodeId,
-							fullMarkdown,
-							sections,
-						}
-					})()
-				: undefined
+		let scope: typeof undefined | {
+			scopeType: ScopeType
+			scopeId: string | null
+			docNodeId: string | null
+			fullMarkdown: string
+			sections?: MarkdownSection[]
+		}
+
+		if (scopeType !== 'chat_thread') {
+			const fullMarkdown = currentDocumentData?.content ?? ''
+			const sections =
+				currentDocumentData?.sections ??
+				(fullMarkdown ? parseMarkdownSections(fullMarkdown) : undefined)
+
+			scope = { scopeType, scopeId, docNodeId, fullMarkdown, sections }
+		}

54-55: Import ActionType and ScopeType from shared types to avoid duplication.

Lines 54–55 locally define types that already exist in apps/pro-web/types/operation-meta.ts. Import them instead to maintain consistency across the codebase.

Suggested import
+import type { ActionType, ScopeType } from '@/types/operation-meta'
 // ...
-type ActionType = 'ask' | 'rewrite' | 'expand' | 'edit'
-type ScopeType = 'chat_thread' | 'doc_root' | 'heading_thread'
apps/pro-web/lib/hooks/use-workspace-chat.integration.test.tsx (1)

119-121: useAsync mock may need adjustment if tests expand.

Returning an empty object works for current tests but may cause issues if future tests depend on useAsync return values like loading, error, or value.

Consider a more complete mock shape
 vi.mock('react-use', () => ({
-	useAsync: () => ({}),
+	useAsync: () => ({
+		loading: false,
+		error: undefined,
+		value: undefined,
+	}),
 }))
apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts (3)

6-9: Duplicate type definition detected.

AssistantAnswerScopeType is defined identically in retrieve-assistant-answer-memories.ts (lines 32-35). Consider extracting this type to a shared types file (e.g., ./types.ts) and importing from there to maintain a single source of truth.

♻️ Suggested refactor

Create a shared types file:

// apps/pro-web/app/actions/chat-memory/types.ts
export type AssistantAnswerScopeType =
  | 'heading_thread'
  | 'doc_root'
  | 'chat_thread'

Then import in both files:

-export type AssistantAnswerScopeType =
-	| 'heading_thread'
-	| 'doc_root'
-	| 'chat_thread'
+import type { AssistantAnswerScopeType } from './types'
+export type { AssistantAnswerScopeType }

104-113: Silent failure on turn index query could mask issues.

When the MaxTurnIndex query fails, the function returns silently without any logging. This makes debugging difficult if there are persistent issues with the Hasura query. Consider adding a warning log before the early return.

♻️ Suggested improvement
 	} catch {
+		console.warn('[ingestAssistantAnswer] Failed to fetch max turn index, aborting ingestion')
 		return
 	}

176-213: Duplicate hasuraGql helper function.

This helper is nearly identical to the one in retrieve-assistant-answer-memories.ts (lines 514-547). Extract to a shared utility module to avoid code duplication and ensure consistent behavior.

♻️ Suggested refactor

Create a shared Hasura utility:

// apps/pro-web/app/actions/chat-memory/hasura-gql.ts
'use server'

export async function hasuraGql<TData>(
  query: string,
  variables?: Record<string, unknown>,
): Promise<TData> {
  // ... implementation
}

Then import in both files:

-async function hasuraGql<TData>(
-  query: string,
-  variables?: Record<string, unknown>,
-): Promise<TData> {
-  // ...
-}
+import { hasuraGql } from './hasura-gql'
apps/pro-web/components/routes/chat/chat-list/index.tsx (2)

46-49: Weakened type safety with unknown[] for messages prop.

Changing messages from a typed array to unknown[] reduces type safety. While the comment explains the intent to normalize internally, this pushes runtime errors from compile-time to runtime.

Consider using a union type that explicitly supports both message shapes:

+import type { UIMessage } from '@ai-sdk/react'
+import type { Message as DBMessage } from 'mb-genql'

 export interface ChatList {
-	// Accept both UIMessage[] (from useChat/@ai-sdk/react) and DB Message[] (mb-genql).
-	// We normalize/cast internally before pairing to preserve existing runtime behavior.
-	messages?: unknown[]
+	messages?: UIMessage[] | DBMessage[] | ThreadMessage[]

185-233: Complex isDocLinkedThread logic could benefit from extraction.

This 48-line useMemo with nested helper functions and multiple fallback checks is difficult to test and maintain. Consider extracting to a pure utility function.

♻️ Suggested refactor

Extract to a testable utility:

// lib/utils/is-doc-linked-thread.ts
export function isDocLinkedThread(
  metadata: Record<string, unknown> | undefined,
  messages: unknown[],
): boolean {
  // Fast signals from thread metadata
  if (metadata) {
    const docNodeId = metadata.docNodeId ?? metadata.doc_node_id
    if (typeof docNodeId === 'string' && docNodeId.trim()) return true
    
    const docs = metadata.documents
    if (Array.isArray(docs) && docs.length > 0) return true
  }

  // Message-level anchors
  return messages.some(hasDocAnchor)
}

function hasDocAnchor(m: unknown): boolean {
  // ... extracted logic
}

Then in the component:

-const isDocLinkedThread = useMemo(() => {
-  // ... 48 lines
-}, [activeThread?.metadata, previousChatMessages, chatMessages])
+const isDocLinkedThread = useMemo(
+  () => isDocLinkedThreadUtil(
+    activeThread?.metadata as Record<string, unknown> | undefined,
+    [...previousChatMessages, ...chatMessages],
+  ),
+  [activeThread?.metadata, previousChatMessages, chatMessages],
+)
apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts (1)

241-320: Retry loop lacks exponential backoff for transient failures.

The 6-iteration retry loop handles schema evolution well, but doesn't distinguish between schema mismatches (which should retry immediately with adjusted params) and transient network failures (which should use backoff). Currently, network failures will exhaust all retries instantly.

Consider adding a small delay for non-schema errors:

// After line 318, before throw
if (attempt < 5) {
  await new Promise(r => setTimeout(r, 100 * (attempt + 1)))
  continue
}
throw e
apps/hasura/migrations/masterbots/1766624501677_assistant_answers/up.sql (1)

59-61: Consider deferring IVFFlat lists parameter tuning until table size is known.

The ivfflat index uses the default lists = 100, which is reasonable for unknown data volumes. Once the assistant_answers table reaches a stable size, you can optimize the lists parameter by rebuilding the index with a value based on expected row count (typically sqrt(row_count) or row_count / 1000). Use CREATE INDEX CONCURRENTLY during production rebuilds to avoid blocking writes. This optimization can be performed incrementally as performance data becomes available.

apps/pro-web/components/shared/shared-accordion.tsx (1)

278-320: Duplicate guard logic should be consolidated.

Lines 278-294 and 296-320 perform the same validation for resolvedThreadSlug, chatbotName, and canonicalDomain. The first guard already returns early if any are missing, making the second check unreachable.

♻️ Proposed fix: Remove duplicate guard
 		// Guard: never call navigateTo with missing required params
 		if (!resolvedThreadSlug || !chatbotName || !canonicalDomain) {
 			if (appConfig.features.devMode) {
 				console.error(
 					'updateActiveThread: missing required params for navigation',
 					{
 						threadId: thread.threadId,
 						chatbotName,
 						canonicalDomain,
 						resolvedThreadSlug,
 						fullThread,
 						thread,
 						paramsThreadSlug: params.threadSlug,
 					},
 				)
 			}
 			return thread
 		}
-
-		// Guard: don't call navigateTo with missing required params (url builder logs + returns "/")
-		const missing: string[] = []
-		if (!resolvedThreadSlug) missing.push('threadSlug')
-		if (!chatbotName) missing.push('chatbot')
-		if (!canonicalDomain) missing.push('domain')
-
-		if (missing.length) {
-			if (appConfig.features.devMode) {
-				console.error(
-					`updateActiveThread: missing required params for navigation: ${missing.join(
-						', ',
-					)}`,
-					{
-						threadId: thread.threadId,
-						chatbotName,
-						canonicalDomain,
-						resolvedThreadSlug,
-						fullThread,
-						thread,
-						paramsThreadSlug: params.threadSlug,
-					},
-				)
-			}
-			return thread
-		}
apps/pro-web/lib/hooks/use-mb-chat.tsx (4)

97-106: Duplicate type definition for MbScopeInput.

MbScopeInput is defined via module augmentation at lines 97-106, then redefined as a local type at lines 331-351. The local definition shadows the augmented interface, which may cause confusion. Consider consolidating into a single exported type.

Also applies to: 331-351


134-181: Consider moving types to module level.

OperationMeta, UiMode, ActionType, etc. are defined inside the MBChatProvider component. This works but:

  1. Types are recreated conceptually on each render (no runtime cost, but unusual)
  2. Cannot be imported/reused elsewhere
  3. Makes the component harder to read

Consider extracting these to module level or a shared types file.


466-489: Heavy use of any in normalizeToolCall.

While the function handles SDK shape variations, the extensive (toolCall as any) casts reduce type safety. Consider defining a union type for the expected SDK shapes.

type SdkToolCall = {
  toolCallId?: string
  id?: string
  toolName?: string
  name?: string
  args?: unknown
  arguments?: unknown
  function?: { name?: string; arguments?: unknown }
}

1488-1493: Silent error swallowing in scoped history fetch.

The catch block at lines 1490-1493 silently sets scopedHistory = null without logging. While fail-open is correct, consider adding a debug log for troubleshooting:

 			} catch {
-				// fail-open: never block chat if the scoped window query fails
-				scopedHistory = null
+				// fail-open: never block chat if the scoped window query fails
+				if (appConfig.features.devMode) {
+					console.warn('selectScopeConversationWindow failed, falling back')
+				}
+				scopedHistory = null
 			}
apps/pro-web/components/routes/workspace/document-tab/document-thread-outline.tsx (1)

94-97: Type is not exported despite being referenced in AI summary.

GroupedPairsResult is declared but not exported. The AI summary indicates it should be exported. If external consumers need this type, add the export keyword.

-type GroupedPairsResult = {
+export type GroupedPairsResult = {
 	pairsByScope: Map<string, MessagePair[]>
 	titleSnapshotByScope: Map<string, string>
 }

Comment on lines +92 to +94
WHERE aa.thread_id = p_thread_id
AND aa.scope_type = p_scope_type
AND aa.scope_id = p_scope_id
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

🧩 Analysis chain

🏁 Script executed:

cat -n apps/hasura/migrations/masterbots/1766957388317__op_meta_and_scoped_rag/up.sql | head -120 | tail -40

Repository: bitcashorg/masterbots

Length of output: 1413


🏁 Script executed:

# Search for the function definition and complete query context
rg -n "scope_id|p_scope_id" apps/hasura/migrations/masterbots/1766957388317__op_meta_and_scoped_rag/up.sql -B5 -A5

Repository: bitcashorg/masterbots

Length of output: 3183


🏁 Script executed:

# Check if there are existing patterns for NULL comparisons in other migrations
fd . apps/hasura/migrations/masterbots --type f -name "*.sql" | xargs rg "IS NOT DISTINCT FROM"

Repository: bitcashorg/masterbots

Length of output: 297


🏁 Script executed:

# Search for scope_type and doc_root references to understand valid values
rg -n "doc_root|scope_type" apps/hasura/migrations/masterbots/1766957388317__op_meta_and_scoped_rag/up.sql -B2 -A2

Repository: bitcashorg/masterbots

Length of output: 2318


🏁 Script executed:

rg "search_assistant_answers_scoped_gql" --type ts --type tsx --type js

Repository: bitcashorg/masterbots

Length of output: 92


🏁 Script executed:

# Also check for any GraphQL queries or mutations that might call this function
rg "searchAssistantAnswersScoped|search_assistant_answers_scoped" apps/pro-web --type ts --type tsx -B3 -A3

Repository: bitcashorg/masterbots

Length of output: 92


🏁 Script executed:

# Check the actual query execution in the codebase
rg "scope_id" apps/pro-web/lib --type ts --type tsx -B2 -A2 | head -100

Repository: bitcashorg/masterbots

Length of output: 92


🏁 Script executed:

rg "search_assistant_answers_scoped_gql|searchAssistantAnswersScoped" apps/

Repository: bitcashorg/masterbots

Length of output: 507


🏁 Script executed:

# Search for scope_id assignments and checks in the codebase
rg "scope_id.*=.*null|scope_id.*=.*NULL" apps/ -i | head -50

Repository: bitcashorg/masterbots

Length of output: 738


🏁 Script executed:

# Check the full schema definition for nullable columns
rg "scope_id text" apps/hasura/migrations/ -B2 -A2

Repository: bitcashorg/masterbots

Length of output: 3100


Use IS NOT DISTINCT FROM for nullable scope_id comparison.

The condition aa.scope_id = p_scope_id will exclude valid rows when both are NULL. The codebase explicitly allows and uses NULL scope_id for doc_root scope type (as documented in ingest-assistant-answer.ts and retrieve-assistant-answer-memories.ts). This causes silent data loss.

Proposed fix
   WHERE aa.thread_id = p_thread_id
     AND aa.scope_type = p_scope_type
-    AND aa.scope_id = p_scope_id
+    AND aa.scope_id IS NOT DISTINCT FROM p_scope_id
     AND (p_exclude_message_id IS NULL OR aa.message_id <> p_exclude_message_id)
📝 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
WHERE aa.thread_id = p_thread_id
AND aa.scope_type = p_scope_type
AND aa.scope_id = p_scope_id
WHERE aa.thread_id = p_thread_id
AND aa.scope_type = p_scope_type
AND aa.scope_id IS NOT DISTINCT FROM p_scope_id
🤖 Prompt for AI Agents
In
@apps/hasura/migrations/masterbots/1766957388317__op_meta_and_scoped_rag/up.sql
around lines 92 - 94, The WHERE clause comparing aa.scope_id = p_scope_id
incorrectly excludes rows when both are NULL; update the condition to use
NULL-safe comparison (aa.scope_id IS NOT DISTINCT FROM p_scope_id) so rows with
both NULL scope_id (used for the doc_root scope_type) are preserved; apply this
change in the query that includes the predicates aa.thread_id = p_thread_id AND
aa.scope_type = p_scope_type AND aa.scope_id = p_scope_id.

Comment on lines +38 to +43
-- assistant_answers: your metadata expects scope_type/scope_id already exist:contentReference[oaicite:0]{index=0}
CREATE INDEX IF NOT EXISTS assistant_answers_thread_doc_scope_idx
ON public.assistant_answers (thread_id, doc_node_id, scope_type, scope_id);

CREATE INDEX IF NOT EXISTS assistant_answers_thread_doc_scope_turn_idx
ON public.assistant_answers (thread_id, doc_node_id, scope_type, scope_id, turn_index);
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

Inconsistent handling: assistant_answers indexes assume scope columns exist.

Lines 39-43 create indexes on assistant_answers assuming scope_type and scope_id exist, but the message table at Lines 16-36 defensively checks first. This could cause migration failures if the columns don't exist on assistant_answers.

Also, Line 38 has a garbled comment artifact (contentReference[oaicite:0]{index=0}) that should be cleaned up.

Add defensive check for assistant_answers
--- assistant_answers: your metadata expects scope_type/scope_id already exist:contentReference[oaicite:0]{index=0}
-CREATE INDEX IF NOT EXISTS assistant_answers_thread_doc_scope_idx
-ON public.assistant_answers (thread_id, doc_node_id, scope_type, scope_id);
-
-CREATE INDEX IF NOT EXISTS assistant_answers_thread_doc_scope_turn_idx
-ON public.assistant_answers (thread_id, doc_node_id, scope_type, scope_id, turn_index);
+-- assistant_answers: create indexes only if scope columns exist
+DO $$
+BEGIN
+  IF EXISTS (
+    SELECT 1 FROM information_schema.columns
+    WHERE table_schema = 'public'
+      AND table_name = 'assistant_answers'
+      AND column_name = 'scope_type'
+  )
+  AND EXISTS (
+    SELECT 1 FROM information_schema.columns
+    WHERE table_schema = 'public'
+      AND table_name = 'assistant_answers'
+      AND column_name = 'scope_id'
+  )
+  THEN
+    CREATE INDEX IF NOT EXISTS assistant_answers_thread_doc_scope_idx
+    ON public.assistant_answers (thread_id, doc_node_id, scope_type, scope_id);
+
+    CREATE INDEX IF NOT EXISTS assistant_answers_thread_doc_scope_turn_idx
+    ON public.assistant_answers (thread_id, doc_node_id, scope_type, scope_id, turn_index);
+  END IF;
+END $$;
📝 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
-- assistant_answers: your metadata expects scope_type/scope_id already exist:contentReference[oaicite:0]{index=0}
CREATE INDEX IF NOT EXISTS assistant_answers_thread_doc_scope_idx
ON public.assistant_answers (thread_id, doc_node_id, scope_type, scope_id);
CREATE INDEX IF NOT EXISTS assistant_answers_thread_doc_scope_turn_idx
ON public.assistant_answers (thread_id, doc_node_id, scope_type, scope_id, turn_index);
-- assistant_answers: create indexes only if scope columns exist
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'assistant_answers'
AND column_name = 'scope_type'
)
AND EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'assistant_answers'
AND column_name = 'scope_id'
)
THEN
CREATE INDEX IF NOT EXISTS assistant_answers_thread_doc_scope_idx
ON public.assistant_answers (thread_id, doc_node_id, scope_type, scope_id);
CREATE INDEX IF NOT EXISTS assistant_answers_thread_doc_scope_turn_idx
ON public.assistant_answers (thread_id, doc_node_id, scope_type, scope_id, turn_index);
END IF;
END $$;
🤖 Prompt for AI Agents
In
@apps/hasura/migrations/masterbots/1767498310895_add_public_message_docid/up.sql
around lines 38 - 43, Remove the garbled comment artifact
"contentReference[oaicite:0]{index=0}" and guard the two index creations
(assistant_answers_thread_doc_scope_idx and
assistant_answers_thread_doc_scope_turn_idx) with a defensive check that the
assistant_answers table actually has both scope_type and scope_id columns;
implement this by wrapping the CREATE INDEX statements in a conditional DO block
or IF EXISTS query that checks information_schema.columns (or pg_catalog) for
both column names on table assistant_answers and only runs the CREATE INDEX ...
statements when both columns are present.

Comment on lines 155 to 173
} catch (error) {
// Retry once without doc_node_id if that was the cause (schema lag during rollout).
if (docNodeId && typeof (error as Error)?.message === 'string') {
const msg = (error as Error).message
if (
msg.includes('doc_node_id') ||
msg.includes('field') ||
msg.includes('assistant_answers_insert_input')
) {
try {
const { doc_node_id: _ignored, ...withoutDocNode } = baseObject as any
await hasuraGql(insert, { object: withoutDocNode })
} catch {
return
}
}
}
return
}
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

Retry logic swallows original error on non-schema-related failures.

When the initial insert fails and the error message doesn't match the schema lag conditions, the function returns silently at line 172, losing the original error context. This makes debugging non-schema-related failures difficult.

🔧 Suggested improvement
 	} catch (error) {
 		// Retry once without doc_node_id if that was the cause (schema lag during rollout).
 		if (docNodeId && typeof (error as Error)?.message === 'string') {
 			const msg = (error as Error).message
 			if (
 				msg.includes('doc_node_id') ||
 				msg.includes('field') ||
 				msg.includes('assistant_answers_insert_input')
 			) {
 				try {
 					const { doc_node_id: _ignored, ...withoutDocNode } = baseObject as any
 					await hasuraGql(insert, { object: withoutDocNode })
+					return // Success on retry
 				} catch {
+					console.warn('[ingestAssistantAnswer] Retry without doc_node_id also failed')
 					return
 				}
 			}
 		}
+		// Log non-schema errors for debugging (fail-closed, no throw)
+		console.warn('[ingestAssistantAnswer] Insert failed:', (error as Error)?.message)
 		return
 	}
🤖 Prompt for AI Agents
In @apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts around lines
155 - 173, The catch block currently swallows the original error when it isn't a
schema-lag case; change it to preserve and surface that error: capture the
caught error in the catch (error) block, only attempt the retry branch when
docNodeId && error.message indicates schema issues (as currently done for
doc_node_id/field/assistant_answers_insert_input), but if the message does not
match those conditions rethrow the original error (or at minimum log it) instead
of returning; additionally, if the retry path is taken and the retry fails,
rethrow or attach both the retry error and the original error so callers see
both contexts (refer to docNodeId, baseObject, hasuraGql, and insert to locate
and update the relevant logic).

Comment on lines +304 to +324
<Button
type="button"
variant={viewMode === 'sections' ? 'secondary' : 'outline'}
size="sm"
onClick={() => setIsPreview(false)}
disabled={isBusy}
>
<FileText className="size-4 mr-2" />
Sections
</Button>

<Button
type="button"
variant={viewMode === 'source' ? 'secondary' : 'outline'}
size="sm"
onClick={() => setIsPreview(false)}
disabled={isBusy}
>
<TextCursorInputIcon className="size-4 mr-2" />
Source
</Button>
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

🧩 Analysis chain

🏁 Script executed:

rg -n "viewMode" --type=ts --type=tsx apps/pro-web/components/routes/workspace/ -C 3

Repository: bitcashorg/masterbots

Length of output: 92


🏁 Script executed:

cat -n apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx | head -350 | tail -100

Repository: bitcashorg/masterbots

Length of output: 3448


🏁 Script executed:

cat -n apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx | head -100

Repository: bitcashorg/masterbots

Length of output: 3879


🏁 Script executed:

rg -n "setViewMode|viewMode\s*=" --type ts apps/pro-web/components/routes/workspace/document-tab/ -C 2

Repository: bitcashorg/masterbots

Length of output: 3189


🏁 Script executed:

cat -n apps/pro-web/components/routes/workspace/document-tab/workspace-document-tab.tsx | head -100

Repository: bitcashorg/masterbots

Length of output: 4419


🏁 Script executed:

rg -n "viewMode" apps/pro-web/components/routes/workspace/document-tab/workspace-document-tab.tsx -C 5

Repository: bitcashorg/masterbots

Length of output: 691


🏁 Script executed:

rg -n "viewMode" apps/pro-web/lib/hooks/ -C 3

Repository: bitcashorg/masterbots

Length of output: 2648


🏁 Script executed:

fd -t f "use-workspace-document-content" apps/pro-web/lib/hooks/

Repository: bitcashorg/masterbots

Length of output: 126


🏁 Script executed:

cat -n apps/pro-web/lib/hooks/use-workspace-document-content-hook.ts | grep -A 50 "return {" | head -60

Repository: bitcashorg/masterbots

Length of output: 1748


🏁 Script executed:

rg -n "setViewMode" apps/pro-web/lib/hooks/use-workspace-document-content-hook.ts -C 3

Repository: bitcashorg/masterbots

Length of output: 653


🏁 Script executed:

rg -n "handleViewSourceToggle" apps/pro-web/lib/hooks/use-workspace-document-content-hook.ts -B 5 -A 10

Repository: bitcashorg/masterbots

Length of output: 1563


🏁 Script executed:

cat -n apps/pro-web/lib/hooks/use-workspace-document-content-hook.ts | sed -n '330,360p'

Repository: bitcashorg/masterbots

Length of output: 1182


🏁 Script executed:

cat -n apps/pro-web/components/routes/workspace/document-tab/workspace-document-tab.tsx | sed -n '60,80p'

Repository: bitcashorg/masterbots

Length of output: 988


🏁 Script executed:

rg -n "handleViewSourceToggle" apps/pro-web/components/routes/workspace/document-tab/ -C 2

Repository: bitcashorg/masterbots

Length of output: 606


Pass handleViewSourceToggle to the component and use it to change viewMode.

The Sections and Source buttons display the viewMode state in their variant styling but cannot change it. Both buttons call setIsPreview(false) only, leaving viewMode unchanged. The parent component extracts handleViewSourceToggle from the hook but doesn't pass it to WorkspaceDocumentTextEditor, so the child has no mechanism to update the view mode.

The buttons should call handleViewSourceToggle('sections') and handleViewSourceToggle('source') respectively. Either pass handleViewSourceToggle as a prop or provide an alternative way for the component to trigger viewMode changes.

🤖 Prompt for AI Agents
In
@apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
around lines 304 - 324, The Sections/Source buttons in
WorkspaceDocumentTextEditor read viewMode for styling but never change it; pass
the parent's handleViewSourceToggle prop into WorkspaceDocumentTextEditor and
call handleViewSourceToggle('sections') for the Sections button and
handleViewSourceToggle('source') for the Source button (you can still call
setIsPreview(false) as before). Update the component signature to accept
handleViewSourceToggle and replace the onClick handlers that currently only call
setIsPreview with calls that invoke handleViewSourceToggle with the correct
mode.

const handleClick = async (e: React.MouseEvent) => {
e.stopPropagation()
const category = thread?.chatbot.department?.name || 'general'
const category = thread?.chatbot?.categories[0]?.category?.name
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

Potential runtime error: accessing categories[0] without null check.

The expression thread?.chatbot?.categories[0]?.category?.name will throw if categories is an empty array since categories[0] returns undefined, but the preceding ?. does not guard array access.

🐛 Proposed fix
-		const category = thread?.chatbot?.categories[0]?.category?.name
+		const category = thread?.chatbot?.categories?.[0]?.category?.name
📝 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 category = thread?.chatbot?.categories[0]?.category?.name
const category = thread?.chatbot?.categories?.[0]?.category?.name
🤖 Prompt for AI Agents
In @apps/pro-web/components/shared/shared-accordion.tsx at line 374, The current
assignment to the local variable "category" can throw when chatbot.categories is
an empty array; change the access to safely handle empty arrays by using
optional chaining on the index (e.g.,
thread?.chatbot?.categories?.[0]?.category?.name) or guard with a length check
(e.g., const categories = thread?.chatbot?.categories; const category =
categories && categories.length ? categories[0].category?.name : undefined) so
that accessing categories[0] never throws; update the assignment where
"category" is declared.

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: 6

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/lib/hooks/use-onboarding.ts (1)

97-97: Remove default export per coding guidelines.

The coding guidelines specify "Avoid default exports; prefer named exports." Since useOnboarding is already exported as a named export at line 15, this default export should be removed.

♻️ Proposed fix
 	return { showOnboarding, markSeen }
 }
-
-export default useOnboarding

As per coding guidelines, named exports are preferred for consistency across the codebase.

apps/pro-web/components/routes/chat/prompt-form/index.tsx (2)

152-152: Remove debug console.log statement.

This console.log('normalizedInput', normalizedInput) appears to be debug output that should be removed before merging.

🧹 Remove debug statement
		const normalizedInput = input.toLowerCase()
-		console.log('normalizedInput', normalizedInput)
		if (

336-337: Remove debug console.log statements in handleAIAction.

Multiple console.log statements are present throughout the handleAIAction callback (lines 336-337, 363, 368, 374, 399). Consider removing these or replacing with a proper logging utility that can be disabled in production.

apps/pro-web/components/routes/browse/browse-list-item.tsx (1)

42-48: Unused state and variable.

  • messages state is declared but setMessages is never called, so messages is always an empty array passed to BrowseChatMessageList.
  • tab is destructured from useBrowse() but never used.

If this is intentional placeholder code, consider adding a TODO comment. Otherwise, remove the unused declarations or implement the message-loading logic.

🧹 Suggested cleanup
-	const [messages, setMessages] = React.useState<Message[]>([])
 	const { searchTerm } = useThreadSearch()
 	const [isAccordionOpen, setIsAccordionOpen] = React.useState(false)
 	const [isVisible, setIsVisible] = React.useState(true)
 	const params = useParams()

-	const { tab } = useBrowse()
+	useBrowse() // Ensure hook is called if needed for side effects

And update the BrowseChatMessageList usage:

 				<BrowseChatMessageList
 					chatbot={thread?.chatbot}
 					user={thread?.user || undefined}
-					messages={messages}
+					messages={thread.messages || []}
 				/>
🤖 Fix all issues with AI agents
In `@apps/pro-web/components/routes/browse/browse-list-item.tsx`:
- Around line 146-148: The comment is wrong: returning null unmounts the
component but the comment claims it stays mounted; either update the comment to
state that "returning null unmounts the component" or change the behavior to
keep the component mounted by replacing the early return of null (the isVisible
check) with rendering the element with a hidden style/class (e.g., use CSS
visibility:hidden or a "hidden" class) so state/scroll are preserved; locate the
isVisible check in the BrowseListItem component (the if (!isVisible) return null
line) and apply one of these two fixes.
- Around line 59-76: There are two duplicate useEffect blocks both writing to
initialUrl; remove the second useEffect (the one without the guard) so only the
guarded useEffect runs and preserves the original URL, ensuring initialUrl is
assigned only once; if instead you intended to update on tab changes, convert to
a single useEffect that includes tab in its dependency array and remove the
early-return guard inside that effect so initialUrl is updated when tab changes.
- Around line 78-123: The parent component’s loadMore callback is unstable and
causes the IntersectionObserver effect in browse-list-item.tsx to re-run; wrap
the loadMore function in the parent (browse-specific-thread-list.tsx) with
useCallback and include only stable dependencies so its reference remains stable
across renders, ensuring the prop named loadMore passed into the BrowseListItem
(or thread list) is memoized and does not trigger unnecessary effect
re-subscriptions.

In
`@apps/pro-web/components/routes/workspace/document-tab/workspace-document-section-item.tsx`:
- Around line 113-126: The Button for deleting a section currently lacks an
explicit type and may default to "submit" inside a form; update the Button
component (the instance rendering XIcon with onClick that calls
onDeleteSection(section.title)) to include type="button" so it cannot trigger
form submission, keeping the existing e.preventDefault()/e.stopPropagation and
accessibility title/aria-label intact.

In `@apps/pro-web/lib/queries/use-document-content.ts`:
- Around line 123-125: The hook useDocumentContent throws when queryDoc is
missing (the Error('Document not found in workspace structure') path), so update
all callers that currently destructure only { data, isLoading, isPending } to
also destructure { isError, error } from useDocumentContent and handle the error
case explicitly (render an error UI or message when isError is true) instead of
silently falling back to documentData?.sections || []; search for usages of
useDocumentContent and adjust their render logic to check isError and use error
for diagnostics.

In `@apps/pro-web/lib/url.ts`:
- Around line 486-503: The fallback URL construction inside the if
(!threadQuestionSlug) branch can produce empty path segments; modify the block
(the fallback branch that currently returns arrays joined with '/') to either
guard early using the existing missingKeys check for required fields or build
the parts array and remove empty segments before joining while preserving the
leading slash (i.e., keep the initial empty string element). Target the fallback
logic in the function in apps/pro-web/lib/url.ts where the chatbot/chat branch
and the longer 'u'/'t' branch are returned and ensure you filter out falsy/empty
string segments (but still preserve the initial leading slash) or return a safe
fallback when required keys are missing.
🧹 Nitpick comments (24)
apps/pro-web/lib/hooks/use-onboarding.ts (3)

16-18: Unused session variable.

The session variable is destructured from useSession() but is never used in this hook. Consider removing this line to avoid confusion and unnecessary hook invocation.

🧹 Proposed cleanup
 export function useOnboarding() {
-	const { data: session } = useSession()
 	const { selectedCategories, isLoaded: isCategoryStorageLoaded } =
 		useCategorySelections()

Also remove the unused import:

-import { useSession } from 'next-auth/react'

34-61: Hardcoded delay and redundant type check.

Two observations:

  1. The 1750ms timeout is a magic number. Consider extracting it as a named constant with a comment explaining why this specific delay is needed (waiting for sidebar mount).
  2. The el instanceof Node check at line 54 is redundant since el instanceof HTMLElement was already verified at line 36, and HTMLElement extends Node.
♻️ Proposed improvements
+const SIDEBAR_MOUNT_DELAY_MS = 1750 // Wait for sidebar element to be rendered
+
 export function useOnboarding() {
 	// ...
 
-	timeoutRef.current = setTimeout(() => {
+	timeoutRef.current = setTimeout(() => {
 		const el = document.querySelector('aside[id$=_sidebar]')
 		if (!(el instanceof HTMLElement)) return
 
 		// Set initial state
 		setSidebarState(el.dataset.state || null)
 
 		const observer = new MutationObserver((mutations) => {
 			// ...
 		})
 
-		// Start observing (extra runtime guard prevents "parameter 1 is not of type 'Node'" crashes)
-		if (el instanceof Node) {
-			observer.observe(el, {
-				attributes: true,
-				attributeFilter: ['data-state'],
-			})
-			observerRef.current = observer
-		}
-	}, 1750)
+		observer.observe(el, {
+			attributes: true,
+			attributeFilter: ['data-state'],
+		})
+		observerRef.current = observer
+	}, SIDEBAR_MOUNT_DELAY_MS)

81-84: Remove commented-out code.

The commented line at line 83 should be removed. Commented-out code adds noise and can be retrieved from version control if needed later.

🧹 Proposed cleanup
 		setShowOnboarding(
 			!hasSeen && selectedCategories.length === 0 && isSidebarOpen,
-			// !hasSeen && selectedCategories.length === 0,
 		)
apps/pro-web/lib/hooks/use-intersection-observer.tsx (2)

20-26: Callback stability depends on caller memoizing onIntersect.

The callback is stable only if onIntersect is memoized by the caller. If callers pass an inline arrow function, this will still cause the callback (and thus the effect) to re-run on every render. Consider documenting this expectation or wrapping onIntersect with useRef to capture the latest value without triggering re-subscriptions.

💡 Alternative: Use ref pattern to avoid re-subscription
+import { type RefObject, useCallback, useEffect, useRef } from 'react'
...
 export function useIntersectionObserver({
 	target,
 	root = null,
 	rootMargin = '0px',
 	threshold = 0,
 	onIntersect,
 	enabled = true,
 }: UseIntersectionObserverProps) {
+	const onIntersectRef = useRef(onIntersect)
+	onIntersectRef.current = onIntersect
+
 	const callback = useCallback<IntersectionObserverCallback>(
 		([entry]) => {
-			onIntersect(Boolean(entry?.isIntersecting))
+			onIntersectRef.current(Boolean(entry?.isIntersecting))
 		},
-		[onIntersect],
+		[],
 	)

47-50: Silent catch may hide debugging information.

The empty catch block silently swallows errors. While this prevents crashes, it could hide issues during development. Consider logging in non-production environments.

💡 Optional: Add development-only warning
 		try {
 			observer.observe(targetEl)
 		} catch (error) {
+			if (process.env.NODE_ENV !== 'production') {
+				console.warn('[useIntersectionObserver] Failed to observe element:', error)
+			}
 			observer.disconnect()
 			return
 		}
apps/pro-web/components/routes/chat/prompt-form/ai-actions-dropdown.tsx (2)

35-35: Remove unused import.

useCallback is imported but never used in the component. This creates unnecessary dead code.

Suggested fix
-import { Fragment, useCallback, useMemo, useState } from 'react'
+import { Fragment, useMemo, useState } from 'react'

123-152: Error messages are constructed but may not be surfaced to users.

The actionPayloads.error object contains helpful error messages (lines 127-135), but when the action is cancelled (actionToTrigger !== 'cancel-action' fails), the error message isn't visibly displayed to the user. The action silently fails to trigger.

Consider showing a toast notification or other feedback when actionToTrigger === 'cancel-action' to inform users they need to select a document section first.

apps/pro-web/lib/workspace-debug.ts (1)

20-27: Flags are static — require page refresh after toggling.

DEBUG_FLAGS is evaluated once at module initialization. Setting window.__DEBUG_WORKSPACE__ = true in devtools after page load won't take effect until refresh. This is fine for performance, but consider clarifying in the documentation (lines 4-8) that a refresh is required after enabling flags.

Alternatively, if runtime toggling is desired, use getters:

♻️ Optional: Use getters for runtime toggle support
 export const DEBUG_FLAGS = {
-	workspace: Boolean(w?.__DEBUG_WORKSPACE__),
-	content: Boolean(w?.__DEBUG_WORKSPACE_CONTENT__),
-	chat: Boolean(w?.__DEBUG_WORKSPACE_CHAT__),
-	markdown: Boolean(w?.__DEBUG_MARKDOWN__),
+	get workspace() {
+		return Boolean(w?.__DEBUG_WORKSPACE__)
+	},
+	get content() {
+		return Boolean(w?.__DEBUG_WORKSPACE_CONTENT__)
+	},
+	get chat() {
+		return Boolean(w?.__DEBUG_WORKSPACE_CHAT__)
+	},
+	get markdown() {
+		return Boolean(w?.__DEBUG_MARKDOWN__)
+	},
 }
apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx (2)

10-14: Unused imports: Tooltip components are never used.

The Tooltip, TooltipContent, and TooltipTrigger imports are not used in this component.

🧹 Remove unused imports
-import {
-	Tooltip,
-	TooltipContent,
-	TooltipTrigger,
-} from '@/components/ui/tooltip'

58-79: Consider edge case: suffix removal relies on exact string match.

The replace(suffix, '') at line 67 only removes the first occurrence. If the user manually edits the input to include the suffix text elsewhere, the toggle state may become inconsistent with the actual input content. This is a minor edge case but worth noting.

apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx (1)

1-142: Significant code duplication with pro-web variant.

This file is nearly identical to apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx. Consider extracting the PromptEnhancer component to a shared package (e.g., packages/ui or a shared components package) to maintain a single source of truth and reduce maintenance burden.

Additionally, the Tooltip, TooltipContent, and TooltipTrigger imports at lines 10-14 are unused and should be removed.

apps/web/components/routes/chat/prompt-form/index.tsx (1)

69-69: Unused import: id from date-fns/locale.

The id import from date-fns/locale does not appear to be used in this file.

🧹 Remove unused import
-import { id } from 'date-fns/locale'
apps/pro-web/components/routes/workspace/document-tab/workspace-document-section-item.tsx (2)

71-74: Provide a meaningful lint suppression reason.

The biome-ignore comment uses a placeholder <explanation> instead of explaining why semantic elements aren't suitable here. Consider documenting the actual reason (e.g., "Using div with role=button to support complex nested interactive content and custom keyboard handling").

Suggested fix
 <div
-  // biome-ignore lint/a11y/useSemanticElements: <explanation>
+  // biome-ignore lint/a11y/useSemanticElements: Using div with role=button to support accordion nesting and custom hover state management
   role="button"
   tabIndex={0}

117-121: Consider extracting the delete handler to a stable function reference.

Per coding guidelines, prefer regular function references over inline arrows in JSX to avoid extra re-renders. This handler could be extracted alongside handleSectionClick.

Suggested refactor
 const handleSectionClick = () => {
   onSectionClick(section.id)
 }

+const handleDeleteClick = (e: React.MouseEvent) => {
+  e.preventDefault()
+  e.stopPropagation()
+  onDeleteSection(section.title)
+}
+
 // ... later in JSX:
 <Button
   ...
-  onClick={(e) => {
-    e.preventDefault()
-    e.stopPropagation()
-    onDeleteSection(section.title)
-  }}
+  onClick={handleDeleteClick}
   ...
 >
apps/pro-web/components/routes/browse/browse-list-item.tsx (3)

23-37: Prefer named export over default export.

As per coding guidelines, avoid default exports and prefer named exports for better refactoring support and explicit imports.

♻️ Suggested fix
-export default function BrowseListItem({
+export function BrowseListItem({

Then update imports in consuming files:

import { BrowseListItem } from '@/components/routes/browse/browse-list-item'

19-21: Consider removing or refactoring module-level mutable state.

initialUrl is a module-level variable that persists across all component instances and renders. The comment indicates it's "currently unused." Module-level mutable state in React components can cause subtle bugs with SSR or when multiple instances exist.

If this is truly unused, remove it along with the associated effects. If needed for future use, consider using a ref within the component instead.


46-46: Remove unused params variable.

useParams() is called but params is never used in the component.

-	const params = useParams()
apps/pro-web/lib/queries/use-workspace-structure.ts (1)

347-359: Type assertion (doc as any).docNodeId bypasses type safety.

The WorkspaceDocumentMetadata type should be extended to include docNodeId rather than using as any. This also applies to the final cast on line 359.

♻️ Suggested approach

Update the WorkspaceDocumentMetadata type to include an optional docNodeId field, then remove the as any casts:

// In types/thread.types.ts or wherever WorkspaceDocumentMetadata is defined
 export interface WorkspaceDocumentMetadata {
   // existing fields...
+  docNodeId?: string | null
 }

Then update the mapping:

 finalStructure.documents = Array.from(mergedDocuments.values()).map(
   (doc) => ({
     ...doc,
-    docNodeId:
-      (doc as any).docNodeId ??
-      getCanonicalDocNodeId({
-        docId: doc.id,
-        project: doc.project,
-        documentName: doc.name,
-      }),
+    docNodeId:
+      doc.docNodeId ??
+      getCanonicalDocNodeId({
+        docId: doc.id,
+        project: doc.project,
+        documentName: doc.name,
+      }),
   }),
-)  as any
+)
apps/pro-web/lib/hooks/use-workspace-chat.tsx (1)

787-848: Scope computation and window fetching is well-structured.

The try/catch ensures the workspace chat continues to function even if scope window fetch fails. The finalUserPrompt fallback on line 847 is correct.

One consideration: the type cast on line 840 (scopedHistory as unknown as any) bypasses type checking. Consider aligning the ScopeConversationWindow return type with what followingQuestionsPrompt expects.

💡 Optional: Improve type alignment

The followingQuestionsPrompt expects Array<MessageDB & { id?: string }>. Consider mapping scopedHistory to match this type explicitly rather than using as unknown as any.

 if (scopedHistory.length) {
   finalUserPrompt = followingQuestionsPrompt(
     userPrompt,
-    scopedHistory as unknown as any,
+    scopedHistory.map(msg => ({
+      ...msg,
+      id: msg.messageId,
+    })),
     '',
   )
 }
apps/pro-web/lib/hooks/use-mb-chat.tsx (2)

117-158: Consider extracting inline types to a dedicated types file.

UiMode, ActionType, IntentSource, DocContextPolicy, and OperationMeta are defined inline within the component. Per the coding guidelines, these should be in a .type.ts file for better organization.

The AI summary references apps/pro-web/types/operation-meta.ts which appears to already define some of these types. Consider importing from there instead:

+import type {
+  UiMode,
+  ActionType,
+  IntentSource,
+  DocContextPolicy,
+  OperationMeta,
+} from '@/types/operation-meta'

-type UiMode = 'chat' | 'workspace'
-type ActionType = 'ask' | 'rewrite' | 'expand' | 'edit'
-// ... rest of inline type definitions

1064-1161: Large duplicated structure in enrichedChatOptions construction.

The mb_op_meta object is constructed identically in both branches of the ternary. Consider extracting to a variable.

♻️ Reduce duplication
+const mbOpMeta = {
+  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,
+}

 const enrichedChatOptions: ChatRequestOptions | undefined =
   chatRequestOptions
     ? ({
         ...chatRequestOptions,
         body: {
           ...((chatRequestOptions as any).body || {}),
-          mb_op_meta: {
-            scope_type: scopeTypeForMeta,
-            scope_id: scopeIdForMeta,
-            // ... rest of fields
-          },
+          mb_op_meta: mbOpMeta,
         },
       } as ChatRequestOptions)
     : ({
         body: {
-          mb_op_meta: {
-            scope_type: scopeTypeForMeta,
-            // ... rest of fields
-          },
+          mb_op_meta: mbOpMeta,
         },
       } as ChatRequestOptions)
apps/pro-web/components/routes/chat/chat-list/message-pair-accordion.tsx (2)

200-236: Focus/scroll effect has correct cleanup but could short-circuit earlier.

The effect properly cleans up the timeout. However, when shouldFocus is true and there's no activeThreadQuestionSlug, the effect continues to check for a DOM element that won't exist.

Minor optimization to short-circuit when no URL slug exists
 	useEffect(() => {
 		let shouldFocus = false

 		if (!activeThreadQuestionSlug) {
 			// If no threadQuestionSlug in URL, focus on the last message of the pair
 			shouldFocus = index === arrayLength - 1 && !isPrevious
+			if (shouldFocus) setIsAccordionFocused(true)
+			return
 		} else {
 			// If threadQuestionSlug exists, focus on the message with that slug
 			shouldFocus = userQuestionSlug === activeThreadQuestionSlug
 		}

 		if (shouldFocus) setIsAccordionFocused(true)
-
-		if (!activeThreadQuestionSlug) return

362-366: Dead code: shouldShowUserMessage is always true.

With shouldShowUserMessage hardcoded to true, the conditional checks at lines 414-417 and 391-395 will never evaluate their alternative branches. If this is intentional for future flexibility, consider adding a brief comment. Otherwise, the dead branches could be removed for clarity.

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

693-730: parsePath mirrors parseRouteFromPathname in message-pair-accordion.tsx.

Both functions parse the same URL patterns but return different shapes. Consider consolidating into a single utility to avoid drift, or document the relationship between them.

Comment on lines +59 to +76
// Preserve original behavior: set initialUrl once, then update when tab changes.
// These are guarded for client-only usage (this file is a client component).
useEffect(() => {
if (initialUrl) return
initialUrl = location.href
}, [initialUrl])
try {
initialUrl = window.location.href
} catch {
// no-op
}
}, [])

// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
initialUrl = location.href
}, [tab])
try {
initialUrl = window.location.href
} catch {
// no-op
}
}, [])
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

Duplicate useEffects with conflicting behavior.

Two nearly identical effects both set initialUrl. The first guards against re-assignment, but the second (Lines 70-76) has no guard and runs on every mount, defeating the first effect's purpose. Additionally, the comment mentions "update when tab changes" but tab is not in the dependency array.

If the intent is to preserve the initial URL, remove the second effect. If it should update when tab changes, add tab to the dependency array of a single effect.

🛠️ Suggested fix (preserve initial URL only)
 	// Preserve original behavior: set initialUrl once, then update when tab changes.
 	// These are guarded for client-only usage (this file is a client component).
 	useEffect(() => {
 		if (initialUrl) return
 		try {
 			initialUrl = window.location.href
 		} catch {
 			// no-op
 		}
 	}, [])
-
-	useEffect(() => {
-		try {
-			initialUrl = window.location.href
-		} catch {
-			// no-op
-		}
-	}, [])
📝 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
// Preserve original behavior: set initialUrl once, then update when tab changes.
// These are guarded for client-only usage (this file is a client component).
useEffect(() => {
if (initialUrl) return
initialUrl = location.href
}, [initialUrl])
try {
initialUrl = window.location.href
} catch {
// no-op
}
}, [])
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
initialUrl = location.href
}, [tab])
try {
initialUrl = window.location.href
} catch {
// no-op
}
}, [])
// Preserve original behavior: set initialUrl once, then update when tab changes.
// These are guarded for client-only usage (this file is a client component).
useEffect(() => {
if (initialUrl) return
try {
initialUrl = window.location.href
} catch {
// no-op
}
}, [])
🤖 Prompt for AI Agents
In `@apps/pro-web/components/routes/browse/browse-list-item.tsx` around lines 59 -
76, There are two duplicate useEffect blocks both writing to initialUrl; remove
the second useEffect (the one without the guard) so only the guarded useEffect
runs and preserves the original URL, ensuring initialUrl is assigned only once;
if instead you intended to update on tab changes, convert to a single useEffect
that includes tab in its dependency array and remove the early-return guard
inside that effect so initialUrl is updated when tab changes.

Comment on lines +78 to +123
// Infinite load observer (crash-hardened)
useEffect(() => {
if (!threadRef.current) return
const observer = new IntersectionObserver(([entry]) => {
if (hasMore && isLast && entry.isIntersecting && !loading) {
const timeout = setTimeout(() => {
loadMore()
clearTimeout(timeout)
}, 150)
const el = threadRef.current
if (!(el instanceof Element)) return

// If there’s nothing more to load or this isn’t the last item, don’t observe.
if (!hasMore || !isLast) return

const observer = new IntersectionObserver((entries) => {
const entry = entries[0]
if (!entry?.isIntersecting) return
if (loading) return
if (!hasMore || !isLast) return

// Prevent duplicate timers / duplicate loadMore calls
if (loadMoreTimerRef.current) return

loadMoreTimerRef.current = setTimeout(() => {
loadMoreTimerRef.current = null
loadMore()
}, 150)

// Stop observing once we've triggered loadMore for this last item
try {
observer.unobserve(entry.target)
} catch {
// no-op (target may already be detached)
}
})
observer.observe(threadRef.current)

try {
observer.observe(el)
} catch {
// If observe throws (rare teardown race / invalid element), ensure no crash.
observer.disconnect()
return
}

return () => {
if (loadMoreTimerRef.current) {
clearTimeout(loadMoreTimerRef.current)
loadMoreTimerRef.current = null
}
observer.disconnect()
}
}, [isLast, hasMore, loading])
}, [hasMore, isLast, loading, loadMore])
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
# First, locate and examine the browse-list-item.tsx file
fd "browse-list-item.tsx" -t f

Repository: bitcashorg/masterbots

Length of output: 178


🏁 Script executed:

#!/bin/bash
# Read the browse-list-item.tsx file from pro-web
wc -l "apps/pro-web/components/routes/browse/browse-list-item.tsx"

Repository: bitcashorg/masterbots

Length of output: 127


🏁 Script executed:

#!/bin/bash
# Read the full browse-list-item.tsx file
cat -n "apps/pro-web/components/routes/browse/browse-list-item.tsx"

Repository: bitcashorg/masterbots

Length of output: 8969


🏁 Script executed:

#!/bin/bash
# Find files that import BrowseListItem from pro-web
rg "import.*BrowseListItem|from.*browse-list-item" apps/pro-web --type=tsx -l

Repository: bitcashorg/masterbots

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Find files that import BrowseListItem from pro-web
rg "import.*BrowseListItem|from.*browse-list-item" apps/pro-web -l

Repository: bitcashorg/masterbots

Length of output: 134


🏁 Script executed:

#!/bin/bash
# Examine the file that uses BrowseListItem
wc -l "apps/pro-web/components/routes/browse/browse-specific-thread-list.tsx"

Repository: bitcashorg/masterbots

Length of output: 138


🏁 Script executed:

#!/bin/bash
# Read the browse-specific-thread-list.tsx file
cat -n "apps/pro-web/components/routes/browse/browse-specific-thread-list.tsx"

Repository: bitcashorg/masterbots

Length of output: 4257


🏁 Script executed:

#!/bin/bash
# Double-check if loadMore uses useCallback anywhere in the parent component
rg -A 10 "const loadMore" "apps/pro-web/components/routes/browse/browse-specific-thread-list.tsx"

Repository: bitcashorg/masterbots

Length of output: 351


LGTM - Robust IntersectionObserver implementation.

The crash-hardened observer with timer-based debouncing effectively prevents duplicate loadMore calls and handles edge cases gracefully. The cleanup function correctly clears both the timer and disconnects the observer.

However, ensure loadMore in the parent component (browse-specific-thread-list.tsx) is wrapped in useCallback with stable dependencies to avoid unnecessary observer re-subscriptions. Currently, loadMore is redefined on every parent render, causing the effect to re-run unnecessarily.

🤖 Prompt for AI Agents
In `@apps/pro-web/components/routes/browse/browse-list-item.tsx` around lines 78 -
123, The parent component’s loadMore callback is unstable and causes the
IntersectionObserver effect in browse-list-item.tsx to re-run; wrap the loadMore
function in the parent (browse-specific-thread-list.tsx) with useCallback and
include only stable dependencies so its reference remains stable across renders,
ensuring the prop named loadMore passed into the BrowseListItem (or thread list)
is memoized and does not trigger unnecessary effect re-subscriptions.

Comment on lines +146 to +148
// Preserve original visibility behavior (component remains mounted, just hidden)
if (!isVisible) return 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

Misleading comment - returning null unmounts the component.

The comment states "component remains mounted, just hidden" but returning null actually unmounts the component from the DOM. If the intent is to keep it mounted but hidden (e.g., to preserve scroll position or state), use CSS visibility instead:

🛠️ Option A: Fix comment to match behavior
-	// Preserve original visibility behavior (component remains mounted, just hidden)
+	// Hide component when search doesn't match (component unmounts)
 	if (!isVisible) return null
🛠️ Option B: Keep mounted but visually hidden
-	// Preserve original visibility behavior (component remains mounted, just hidden)
-	if (!isVisible) return null
-
 	return (
-		<div ref={threadRef}>
+		<div ref={threadRef} className={cn({ hidden: !isVisible })}>
📝 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
// Preserve original visibility behavior (component remains mounted, just hidden)
if (!isVisible) return null
// Hide component when search doesn't match (component unmounts)
if (!isVisible) return null
🤖 Prompt for AI Agents
In `@apps/pro-web/components/routes/browse/browse-list-item.tsx` around lines 146
- 148, The comment is wrong: returning null unmounts the component but the
comment claims it stays mounted; either update the comment to state that
"returning null unmounts the component" or change the behavior to keep the
component mounted by replacing the early return of null (the isVisible check)
with rendering the element with a hidden style/class (e.g., use CSS
visibility:hidden or a "hidden" class) so state/scroll are preserved; locate the
isVisible check in the BrowseListItem component (the if (!isVisible) return null
line) and apply one of these two fixes.

Comment on lines 113 to 126
<Button
type="button"
variant="ghost"
size="sm"
className="size-6 p-0 text-foreground/70 hover:text-destructive-foreground hover:bg-destructive"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
onDeleteSection(section.title)
}}
title={`Delete ${section.title} section`}
// disabled={isLoading || workspaceProcessingState !== 'idle'}
aria-label={`Delete ${section.title} section`}
>
<XIcon className="size-3.5" />
</Button>
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

Add type="button" to prevent unintended form submissions.

The type="button" attribute was removed from this Button. Without it, the button defaults to type="submit" when nested inside a form element, which could trigger unintended form submissions despite the e.preventDefault() call.

Proposed fix
 <Button
   variant="ghost"
   size="sm"
+  type="button"
   className="size-6 p-0 text-foreground/70 hover:text-destructive-foreground hover:bg-destructive"
   onClick={(e) => {
     e.preventDefault()
     e.stopPropagation()
     onDeleteSection(section.title)
   }}
   title={`Delete ${section.title} section`}
   aria-label={`Delete ${section.title} section`}
 >
🤖 Prompt for AI Agents
In
`@apps/pro-web/components/routes/workspace/document-tab/workspace-document-section-item.tsx`
around lines 113 - 126, The Button for deleting a section currently lacks an
explicit type and may default to "submit" inside a form; update the Button
component (the instance rendering XIcon with onClick that calls
onDeleteSection(section.title)) to include type="button" so it cannot trigger
form submission, keeping the existing e.preventDefault()/e.stopPropagation and
accessibility title/aria-label intact.

Comment on lines +123 to +125
if (!queryDoc) {
throw new Error('Document not found in workspace structure')
}
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:

# Check how useDocumentContent is used and whether callers handle errors
rg -n --type=tsx --type=ts -A 5 'useDocumentContent\(' apps/pro-web/

Repository: bitcashorg/masterbots

Length of output: 92


🏁 Script executed:

# Also check the full implementation of use-document-content.ts to understand the context
cat -n apps/pro-web/lib/queries/use-document-content.ts

Repository: bitcashorg/masterbots

Length of output: 26292


🏁 Script executed:

# Search for error handling patterns in the codebase (try/catch around useDocumentContent or error boundary usage)
rg -n --type=tsx --type=ts 'try\s*\{' apps/pro-web/ | head -20

Repository: bitcashorg/masterbots

Length of output: 92


🏁 Script executed:

# Search for useDocumentContent usage across TypeScript files
rg -n 'useDocumentContent\(' apps/pro-web/ -A 5

Repository: bitcashorg/masterbots

Length of output: 5089


🏁 Script executed:

# Also search for error handling patterns in components that might use this hook
rg -n 'useDocumentContent\|\.isError\|\.error' apps/pro-web/ | head -30

Repository: bitcashorg/masterbots

Length of output: 47


🏁 Script executed:

# Check if there are error boundaries in the app
rg -n 'ErrorBoundary|error\.tsx|catch' apps/pro-web/app -A 3 | head -40

Repository: bitcashorg/masterbots

Length of output: 3235


🏁 Script executed:

# Look at how callers actually handle the query result - check for error handling
rg -n 'useDocumentContent\(' apps/pro-web/ -A 20 | grep -E 'isError|\.error|loading|isPending|isLoading'

Repository: bitcashorg/masterbots

Length of output: 380


🏁 Script executed:

# Examine one of the main calling components to see how they use the hook
head -100 apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx

Repository: bitcashorg/masterbots

Length of output: 3179


🏁 Script executed:

# Check how data is being used in one of the callers - look for null checks or error handling
sed -n '50,100p' apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx

Repository: bitcashorg/masterbots

Length of output: 1582


🏁 Script executed:

# Search for any null coalescing or fallback handling for document data
rg -n 'currentDocumentData|documentData|documentContent' apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx -A 3 -B 1

Repository: bitcashorg/masterbots

Length of output: 416


🏁 Script executed:

# Check if any of the calling components check for .isError or .error
rg -n '\.isError|\.error' apps/pro-web/components/routes/workspace/document-tab/ -B 2 -A 2

Repository: bitcashorg/masterbots

Length of output: 600


🏁 Script executed:

# Check the wrapper hook use-workspace-document-content-hook.ts to see how it handles errors
cat -n apps/pro-web/lib/hooks/use-workspace-document-content-hook.ts

Repository: bitcashorg/masterbots

Length of output: 21631


🏁 Script executed:

# Look at use-workspace-chat.tsx to see how it uses the result
sed -n '140,180p' apps/pro-web/lib/hooks/use-workspace-chat.tsx

Repository: bitcashorg/masterbots

Length of output: 1328


Ensure error state is handled in calling components.

The error thrown when queryDoc is not found is appropriate for the TanStack Query pattern. However, callers destructure { data, isLoading, isPending } without checking the .isError or .error properties. Components use fallbacks like documentData?.sections || [], which won't help when the query is in an error state—they should explicitly check for .isError and display appropriate error UI instead of silently falling back to empty values.

🤖 Prompt for AI Agents
In `@apps/pro-web/lib/queries/use-document-content.ts` around lines 123 - 125, The
hook useDocumentContent throws when queryDoc is missing (the Error('Document not
found in workspace structure') path), so update all callers that currently
destructure only { data, isLoading, isPending } to also destructure { isError,
error } from useDocumentContent and handle the error case explicitly (render an
error UI or message when isError is true) instead of silently falling back to
documentData?.sections || []; search for usages of useDocumentContent and adjust
their render logic to check isError and use error for diagnostics.

Comment on lines +486 to 503
// If missing, fall back to the parent profile thread URL (no error).
if (!threadQuestionSlug) {
if (type === 'chatbot') {
return ['', 'b', chatbot?.toLowerCase() ?? '', threadSlug ?? ''].join(
'/',
)
return '/'
}
return [
'',
'u',
usernameSlug ?? '',
't',
normalizeCategorySlug(category ?? ''),
normalizeDomainSlug(domain ?? '', raw),
chatbot?.toLowerCase() ?? '',
threadSlug ?? '',
].join('/')
}
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

Fallback URL may produce malformed paths with empty segments.

When threadQuestionSlug is missing and fallback kicks in, if other params (like usernameSlug, category, etc.) are also empty, the URL could contain empty segments (e.g., /u//t///). While this is an edge case, consider adding a guard or using the existing missingKeys check before constructing the fallback.

Suggested defensive check
 			// If missing, fall back to the parent profile thread URL (no error).
 			if (!threadQuestionSlug) {
 				if (type === 'chatbot') {
+					if (!chatbot || !threadSlug) return '/'
 					return ['', 'b', chatbot?.toLowerCase() ?? '', threadSlug ?? ''].join(
 						'/',
 					)
 				}
+				if (!usernameSlug || !category || !domain || !chatbot || !threadSlug) return '/'
 				return [
 					'',
 					'u',
🤖 Prompt for AI Agents
In `@apps/pro-web/lib/url.ts` around lines 486 - 503, The fallback URL
construction inside the if (!threadQuestionSlug) branch can produce empty path
segments; modify the block (the fallback branch that currently returns arrays
joined with '/') to either guard early using the existing missingKeys check for
required fields or build the parts array and remove empty segments before
joining while preserving the leading slash (i.e., keep the initial empty string
element). Target the fallback logic in the function in apps/pro-web/lib/url.ts
where the chatbot/chat branch and the longer 'u'/'t' branch are returned and
ensure you filter out falsy/empty string segments (but still preserve the
initial leading slash) or return a safe fallback when required keys are missing.

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: 3

🤖 Fix all issues with AI agents
In `@apps/pro-web/app/actions/document-operations/log-document-operation.ts`:
- Around line 42-53: In getHasuraConfig, remove the NEXT_PUBLIC fallback for the
admin secret: stop using process.env.NEXT_PUBLIC_HASURA_ADMIN_SECRET and only
read server-side variables (process.env.HASURA_GRAPHQL_ADMIN_SECRET and
process.env.HASURA_ADMIN_SECRET) so the adminSecret cannot be exposed to the
client; update the adminSecret assignment in getHasuraConfig accordingly and
ensure the url fallback to NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT is left only if
intentionally needed for runtime, otherwise use the server-only
HASURA_GRAPHQL_ENDPOINT.

In `@apps/pro-web/lib/hooks/use-workspace-chat.tsx`:
- Around line 246-254: Remove the unnecessary "as any" casts on newDocument: use
the typed properties directly (newDocument?.currentVersion and
newDocument?.versions). Compute versionAfter by checking typeof
newDocument?.currentVersion === 'number' and assign newDocument.currentVersion
or null. For checksumAfter, call newDocument?.versions?.find with the callback
parameter typed as WorkspaceDocumentVersion (e.g., (v: WorkspaceDocumentVersion)
=> v?.version === versionAfter) and read ?.checksum ?? null. This change affects
the newDocument, versionAfter and checksumAfter expressions created after the
useUpdateDocumentContent result.
- Around line 959-963: The double-cast bypass in the call to
followingQuestionsPrompt should be replaced by constructing scopedHistory with
the correct MessageDB shape (Array<MessageDB & { id?: string }>) instead of
using "scopedHistory as unknown as any"; locate where scopedHistory is built in
use-workspace-chat.tsx and map each item to include the MessageDB fields (role,
content, messageId -> id if needed, createdAt) or write a small transform
function (e.g., mapScopedToMessageDB) that converts the existing objects to
MessageDB objects, then pass the transformed array into followingQuestionsPrompt
so TypeScript types line up without unsafe casts (mirror the correct typing
pattern from use-mb-chat.tsx but implement a proper mapping rather than double
casting).
🧹 Nitpick comments (3)
apps/pro-web/lib/hooks/use-workspace-chat.tsx (1)

242-267: Good best-effort logging pattern, but consider consolidating.

The completion logging logic is correctly wrapped in try/catch to ensure it never blocks UX. However, this pattern is repeated in multiple places (lines 242-267, 758-774, 1007-1022).

Consider extracting to a helper function to reduce duplication:

♻️ Optional: Extract logging helper
async function finalizeDOcOpLogging(
  opIdPromise: Promise<string | null> | null,
  mode: 'complete' | 'fail',
  data?: { versionAfter?: number | null; checksumAfter?: string | null; error?: string | null }
) {
  try {
    const opId = await opIdPromise
    if (opId) {
      await logDocumentOperation({
        mode,
        id: opId,
        ...data,
      })
    }
  } catch {
    // ignore - best effort
  }
}
apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/up.sql (1)

74-88: Consider partial index for nullable columns.

The indexes on (doc_node_id, section_id) and (doc_node_id, scope_type, scope_id, ...) include nullable columns. If queries frequently filter for non-NULL values, partial indexes could be more efficient:

CREATE INDEX ... WHERE section_id IS NOT NULL;

This is a minor optimization and can be deferred if query patterns aren't yet established.

apps/pro-web/app/actions/document-operations/log-document-operation.ts (1)

55-82: Consider using getHasuraClient() from mb-lib or hasura.service.ts pattern.

The codebase provides established utilities for Hasura GraphQL operations. The getHasuraClient() function exported from mb-lib includes comprehensive environment validation and error handling for multiple secret variants (HASURA_GRAPHQL_LOCAL_ADMIN_SECRET vs HASURA_GRAPHQL_ADMIN_SECRET). For consistency with the server-side pattern established in apps/pro-web/services/hasura/hasura.service.ts, refactor this custom fetch wrapper to use the shared client instead.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d6b3de and 5f46356.

📒 Files selected for processing (5)
  • apps/hasura/metadata/databases/masterbots/tables/public_document_operations.yaml
  • apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/down.sql
  • apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/up.sql
  • apps/pro-web/app/actions/document-operations/log-document-operation.ts
  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
🧰 Additional context used
📓 Path-based instructions (9)
apps/hasura/migrations/masterbots/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Store and manage Hasura migrations under apps/hasura/migrations/masterbots/

Store Hasura-managed migrations in timestamped folders under apps/hasura/migrations/masterbots/

Keep Hasura-managed database migrations in timestamped folders under apps/hasura/migrations/masterbots/

Files:

  • apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/down.sql
  • apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/up.sql
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Use lowercase with dash-separated directory and file names; use specific extensions: .config.ts, .test.ts, .context.tsx, .type.ts, .hook.ts; components may omit extension
Use Bun for package management and scripts instead of npm/yarn
Use Biome for formatting and linting via bun format-and-lint:fix; avoid Prettier/ESLint configs

Files:

  • apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/down.sql
  • apps/hasura/metadata/databases/masterbots/tables/public_document_operations.yaml
  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
  • apps/pro-web/app/actions/document-operations/log-document-operation.ts
  • apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/up.sql
apps/{web,pro-web}/lib/hooks/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

apps/{web,pro-web}/lib/hooks/**: Place custom React hooks under lib/hooks/
Encapsulate complex state in custom hooks and separate presentation from business logic

Store custom React hooks under lib/hooks/

Files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
apps/{web,pro-web}/**/*.tsx

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

apps/{web,pro-web}/**/*.tsx: Declare React components with the function keyword
Name React components using PascalCase
Order component files: imports → constants → component → styled components → types/interfaces
Use functional components and hooks; type props with a TypeScript interface
Use regular function references (not inline arrows) for React event handlers to avoid extra re-renders

apps/{web,pro-web}/**/*.tsx: Use regular function references for event handlers instead of inline arrow functions in JSX
Declare React components with the function keyword (function ComponentName() {})
Use PascalCase for React component names
Order React component files: imports, constants, component, styled components, types/interfaces
Style components with Tailwind CSS utility classes

Files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Avoid default exports; prefer named exports
Use meaningful variable names; prefix booleans with is/has/does/should
Use interface for objects/classes; type for unions/tuples/aliases; enum for fixed sets; const for literals
Avoid any; prefer specific types
Leverage type inference where clear; annotate function params and return types
Prefer type assertions with 'as' over angle-bracket syntax
Use type guards to narrow types in conditionals

**/*.{ts,tsx}: Avoid default exports; prefer named exports
Use modern JavaScript features with TypeScript across the codebase
Use meaningful names; prefix booleans with is/has/does/should (e.g., isDisabled)
Receive an object and return an object (RORO) for functions interfacing with external services
Type definitions: use interface for objects/classes; type for unions/tuples/aliases; const for literals; enum for fixed sets
Avoid any; prefer specific types
Leverage type inference; omit explicit annotations when obvious
Add explicit type annotations for function parameters and return values
Prefer type assertions with as over angle-bracket syntax
Use type guards to narrow types within conditionals
Favor composition over inheritance in code structure

**/*.{ts,tsx}: Avoid default exports; prefer named exports
Prefer RORO (Receive an object, return an object) for function signatures, especially for external service interactions
Use interface for object shapes/classes; type for unions/tuples/aliases; const for literals; enum for fixed enumerations
Avoid using any; prefer precise types
Leverage type inference when clear; omit redundant annotations
Explicitly annotate function parameters and return types
Prefer type assertions with as over angle-bracket syntax
Use type guards to narrow types safely
Use meaningful variable names; prefix booleans with is/has/does/should
Favor composition over inheritance
Use lowercase dash-separated names for files and folders; use extensions like .config.ts, .test.ts, .context.tsx, .typ...

Files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
  • apps/pro-web/app/actions/document-operations/log-document-operation.ts
apps/{web,pro-web}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

apps/{web,pro-web}/**/*.{ts,tsx}: Use lowercase kebab-case for directories and filenames (components can omit extension)
Verify DOM operations and consider timing (RAF/delays) when manipulating the DOM in React
Use Tailwind CSS utility classes for styling React components

apps/{web,pro-web}/**/*.{ts,tsx}: Use TypeScript to define React props types (interfaces for props)
Use custom hooks (useMBChat, useThread, useSidebar, useModel) for domain-specific state and keep providers focused and composed hierarchically
Separate presentation from business logic and compose small components
Use hasura.service.ts for all GraphQL operations from UI layers

apps/{web,pro-web}/**/*.{ts,tsx}: Prefer Server Components; use the use client directive only when client-side features are required
Implement Nested Layouts using the App Router
Use Streaming and Suspense in Next.js 14/15 where appropriate
Follow Next.js file conventions as per documentation
Use IndexedDB for local caching to enable immediate UI updates

Files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
  • apps/pro-web/app/actions/document-operations/log-document-operation.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

**/*.tsx: Avoid inline arrow functions in JSX props; use stable function references for event handlers
Declare React components with the function keyword (functional components)
Use PascalCase for React component names
Use functional components and hooks instead of class components
Type component props with TypeScript interfaces

Files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
apps/{web,pro-web}/lib/hooks/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Place custom React hooks in lib/hooks/

Files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
apps/{web,pro-web}/app/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

apps/{web,pro-web}/app/**: Prefer Next.js Server Components; use 'use client' only when necessary
Leverage Streaming and Suspense for responsiveness in Next.js 15
Follow Next.js file conventions in the App Router

apps/{web,pro-web}/app/**: Leverage Server Components by default; use the use client directive only when client-side features are needed
Implement Nested Layouts using the App Router
Use Streaming and Suspense features in Next.js 14/15 where appropriate
Follow Next.js file conventions for routing and layout in the App Router
Optimize data fetching: fetch on the server, fetch where needed, and use preload patterns to avoid waterfalls

Files:

  • apps/pro-web/app/actions/document-operations/log-document-operation.ts
🧠 Learnings (25)
📓 Common learnings
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.
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.
📚 Learning: 2024-12-05T01:08:50.085Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2024-12-05T01:08:50.085Z
Learning: The backend uses Hasura with proper table relationships, permissions, and metadata structure defined in YAML configurations.

Applied to files:

  • apps/hasura/metadata/databases/masterbots/tables/public_document_operations.yaml
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to apps/{web,pro-web}/**/*.{ts,tsx} : Use custom hooks (useMBChat, useThread, useSidebar, useModel) for domain-specific state and keep providers focused and composed hierarchically

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2025-10-24T20:40:56.510Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 573
File: apps/pro-web/components/routes/pro/pro.tsx:49-49
Timestamp: 2025-10-24T20:40:56.510Z
Learning: In apps/pro-web/lib/hooks/use-mb-chat.tsx, the useMBChat() hook returns a tuple [MBChatHookState, MBChatHookActions], not a direct object. Use array destructuring to access state (index 0) and actions (index 1), e.g., const [state, actions] = useMBChat() or const [{ isLoading }] = useMBChat().

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2025-02-17T16:51:19.102Z
Learnt from: Bran18
Repo: bitcashorg/masterbots PR: 376
File: apps/masterbots.ai/components/routes/chat/chat.tsx:92-102
Timestamp: 2025-02-17T16:51:19.102Z
Learning: For the Chat component in apps/masterbots.ai/components/routes/chat/chat.tsx, the developer prefers to verify potential optimizations through local testing before accepting suggestions, particularly for performance-related changes like useCallback usage in debounce implementations.

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2024-11-14T21:12:19.789Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 302
File: apps/masterbots.ai/lib/hooks/use-mb-chat.ts:0-0
Timestamp: 2024-11-14T21:12:19.789Z
Learning: In `apps/masterbots.ai/lib/hooks/use-mb-chat.ts`, for handling message ordering issues, prefer simple solutions like using `setTimeout` over adding sequence numbers to the message schema, to keep the codebase simple.

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2025-09-04T21:31:18.830Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2025-09-04T21:31:18.830Z
Learning: The pro-web workspace system has identified performance bottlenecks in markdown parsing, AI streaming response handling, selection range management, Base64 document processing, and multiple debounced state operations that require optimization and better coordination.

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2025-09-04T21:31:18.830Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2025-09-04T21:31:18.830Z
Learning: The pro-web workspace features have complex performance considerations including markdown parsing optimization, AI streaming response handling, state persistence coordination, and selection range management that require careful monitoring and optimization.

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Pro workspace editors: controlled/uncontrolled patterns, Markdown utilities, section state sync, and timed DOM operations

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2025-06-13T08:55:45.217Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 508
File: apps/masterbots.ai/lib/helpers/ai-helpers.ts:280-283
Timestamp: 2025-06-13T08:55:45.217Z
Learning: In `apps/masterbots.ai/lib/helpers/ai-helpers.ts` the `verifyDuplicateMessage` function should:
• Use `message.content` as the sole de-duplication key so provisional (no-slug) and persisted (slugged) messages merge properly.
• Return the full `message` object (not just a string key) for the continuation prompts (`CONTINUE_GENERATION_PROMPT`, `CONTINUE_GENERATION_PROMPT_2`) because their content repeats and they must remain distinct.
• No explicit `!message.content` guard is needed; `return message.content` already yields `undefined` when content is missing.

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2025-07-02T19:46:45.701Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 519
File: apps/masterbots.ai/lib/hooks/use-chat-attachments.ts:276-297
Timestamp: 2025-07-02T19:46:45.701Z
Learning: In `apps/masterbots.ai/lib/hooks/use-chat-attachments.ts`, the `validateTextContent` function intentionally uses `slugify()` with character removal (`remove: /[^\w\s]/g`) followed by splitting on hyphens to count words. This approach is preferred over simple whitespace splitting because it removes special characters (like those found in code text) and counts only "true words", making it more robust for validating pasted content that may contain mixed text and code.

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2024-10-23T00:00:06.253Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 287
File: apps/masterbots.ai/components/routes/chat/share-button.tsx:10-29
Timestamp: 2024-10-23T00:00:06.253Z
Learning: When implementing the `handleShare` function in `apps/masterbots.ai/components/routes/chat/share-button.tsx`, prefer using modern APIs like the Clipboard API over deprecated methods like `document.execCommand('copy')`, as the project no longer needs to support very old web browsers.

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2025-09-04T21:31:18.830Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2025-09-04T21:31:18.830Z
Learning: Successfully created comprehensive GitHub issue `#555` consolidating performance feedback and edge-case management for pro-web workspace features, including prioritized action items, testing requirements, and specific file references for implementation.

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2025-09-04T21:31:18.830Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2025-09-04T21:31:18.830Z
Learning: Edge cases for pro-web workspace include concurrent document editing, AI streaming interruptions, large document handling, selection range boundary conditions, and storage failure scenarios that need comprehensive testing and error handling.

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2024-10-14T18:40:46.645Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 283
File: apps/masterbots.ai/components/routes/chat/admin-mode-approve.tsx:11-15
Timestamp: 2024-10-14T18:40:46.645Z
Learning: In the project, the `adminApproveThread` function within the `useThread` hook manages error handling and loading state, so components using it don't need to implement additional error handling unless they introduce side effects.

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2025-11-21T18:00:08.455Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 590
File: apps/web/lib/hooks/use-profile.tsx:60-78
Timestamp: 2025-11-21T18:00:08.455Z
Learning: In apps/web/lib/hooks/use-profile.tsx: The sessionUser state should be populated directly from session.user (JWT) and not by fetching from Hasura on the client side. The useEffect should only synchronize sessionUser with session.user. Client-side Hasura fetches for user data should be avoided; user data comes from the JWT/session. Only when users update their profile should we refresh both the database and JWT token (via NextAuth session update trigger).

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2024-10-23T07:33:36.041Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 287
File: apps/masterbots.ai/lib/hooks/use-thread-visibility.tsx:3-3
Timestamp: 2024-10-23T07:33:36.041Z
Learning: Always use explicit imports for React hooks to maintain consistency across the monorepo, and avoid using both named and default imports simultaneously.

Applied to files:

  • apps/pro-web/lib/hooks/use-workspace-chat.tsx
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to **/services/hasura.service.ts : Use hasura.service.ts as the single interaction point for GraphQL operations with Hasura

Applied to files:

  • apps/pro-web/app/actions/document-operations/log-document-operation.ts
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to apps/{web,pro-web}/**/*.{ts,tsx} : Use hasura.service.ts for all GraphQL operations from UI layers

Applied to files:

  • apps/pro-web/app/actions/document-operations/log-document-operation.ts
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to **/services/hasura.service.ts : Use hasura.service.ts as the single point for all GraphQL interactions

Applied to files:

  • apps/pro-web/app/actions/document-operations/log-document-operation.ts
📚 Learning: 2025-05-06T09:54:25.327Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 468
File: apps/masterbots.ai/app/api/cron/delete-requested-accounts/route.ts:70-81
Timestamp: 2025-05-06T09:54:25.327Z
Learning: For Hasura GraphQL operations in server-side code (including API routes and cron jobs), use the `getHasuraClient()` function from mb-lib rather than implementing custom fetch functions. This provides proper error handling, environment validation, and follows the established pattern in the codebase.

Applied to files:

  • apps/pro-web/app/actions/document-operations/log-document-operation.ts
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to apps/hasura/migrations/masterbots/** : Keep Hasura-managed database migrations in timestamped folders under apps/hasura/migrations/masterbots/

Applied to files:

  • apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/up.sql
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to apps/hasura/migrations/masterbots/** : Store Hasura-managed migrations in timestamped folders under apps/hasura/migrations/masterbots/

Applied to files:

  • apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/up.sql
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to apps/hasura/migrations/masterbots/** : Store and manage Hasura migrations under apps/hasura/migrations/masterbots/

Applied to files:

  • apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/up.sql
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Perform database schema changes via Hasura console (task console), then migrations (task migrate)

Applied to files:

  • apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/up.sql
🔇 Additional comments (11)
apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/down.sql (1)

1-24: LGTM! Safe and idempotent rollback migration.

The down migration correctly:

  • Drops indexes before the table (avoiding dependency errors)
  • Uses IF EXISTS guards for idempotent execution
  • Safely checks for FK constraint existence before dropping
  • Follows the established Hasura migration patterns in timestamped folders
apps/hasura/metadata/databases/masterbots/tables/public_document_operations.yaml (1)

35-38: Verify access to rows with NULL thread_id.

The filter thread.user_id._eq: X-Hasura-User-Id traverses the object relationship, but thread_id is nullable per the migration (some operations may start before a thread_id is known). Rows with thread_id = NULL will fail this filter since there's no related thread row, making them inaccessible to users.

If orphan rows should be accessible, consider adjusting the filter:

filter:
  _or:
    - thread:
        user_id:
          _eq: X-Hasura-User-Id
    - thread_id:
        _is_null: true

Otherwise, if this is intentional (orphan ops should not be queryable), this is fine as-is.

apps/pro-web/app/actions/document-operations/log-document-operation.ts (2)

84-129: LGTM on the start operation flow.

The insert mutation is well-structured:

  • Properly maps camelCase args to snake_case DB columns
  • Sets sensible defaults with ?? null fallbacks
  • Returns the operation ID for subsequent updates
  • Best-effort pattern with caller-side error handling is appropriate

131-157: LGTM on the complete/fail operation flow.

The update mutation correctly:

  • Determines status from mode
  • Uses pk_columns for targeted update
  • Provides default error message for fail mode
  • Returns updated ID for confirmation
apps/pro-web/lib/hooks/use-workspace-chat.tsx (5)

51-60: LGTM on UUID validation helper.

The asUuidOrNull function is a good defensive measure to prevent noisy Hasura mutation errors when thread IDs may not be valid UUIDs (e.g., during initial state).


62-81: LGTM on operation type inference.

The inferOperationType function provides reasonable heuristics for classifying operations. The priority order (selection → keyword matching) is sensible.


869-874: Verify getCanonicalDocNodeId handles all edge cases.

The fallback chain (documentData as any)?.docNodeId ?? (documentData as any)?.id is reasonable, but the function call could return null. The subsequent fallback on line 877 (docRootId ?? documentKey) ensures a value, which is good.


927-969: Good fail-closed pattern for scope conversation window.

The try/catch ensures workspace chat continues working even if the scope window fetch fails, falling back to the original userPrompt. This is the correct resilience pattern.


992-999: LGTM on scope passthrough to append.

The scope metadata is correctly passed through to enable downstream processing. The structure matches the pattern mentioned in the AI summary for use-mb-chat.tsx.

apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/up.sql (2)

1-44: LGTM! Well-structured table definition.

The table design is solid:

  • UUID primary key with gen_random_uuid() default
  • Appropriate column types and nullability
  • Sensible default for status column
  • Good separation of concerns (context, anchoring, operation, prompts, versioning)
  • IF NOT EXISTS makes the migration idempotent

46-72: Good defensive FK constraint pattern.

The conditional FK creation is well-implemented:

  • Checks if thread table exists before adding constraint
  • Checks if constraint already exists to prevent errors on re-run
  • ON DELETE SET NULL is appropriate for optional relationships

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +42 to +53
function getHasuraConfig() {
// Keep flexible: supports both server-only and next-public env patterns.
const url =
process.env.HASURA_GRAPHQL_ENDPOINT ??
process.env.NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT
const adminSecret =
process.env.HASURA_GRAPHQL_ADMIN_SECRET ??
process.env.HASURA_ADMIN_SECRET ??
process.env.NEXT_PUBLIC_HASURA_ADMIN_SECRET

return { url, adminSecret }
}
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

Remove NEXT_PUBLIC_* fallbacks for admin secret.

The getHasuraConfig function falls back to NEXT_PUBLIC_HASURA_ADMIN_SECRET (line 50). Environment variables prefixed with NEXT_PUBLIC_ are exposed to the browser bundle. While this file uses 'use server', the fallback pattern is risky if copy-pasted elsewhere and contradicts security best practices.

Since this is a server action, only server-side env vars should be used.

🔒 Suggested fix
 function getHasuraConfig() {
-	// Keep flexible: supports both server-only and next-public env patterns.
 	const url =
-		process.env.HASURA_GRAPHQL_ENDPOINT ??
-		process.env.NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT
+		process.env.HASURA_GRAPHQL_ENDPOINT
 	const adminSecret =
-		process.env.HASURA_GRAPHQL_ADMIN_SECRET ??
-		process.env.HASURA_ADMIN_SECRET ??
-		process.env.NEXT_PUBLIC_HASURA_ADMIN_SECRET
+		process.env.HASURA_GRAPHQL_ADMIN_SECRET ??
+		process.env.HASURA_ADMIN_SECRET
 
 	return { url, adminSecret }
 }
📝 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
function getHasuraConfig() {
// Keep flexible: supports both server-only and next-public env patterns.
const url =
process.env.HASURA_GRAPHQL_ENDPOINT ??
process.env.NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT
const adminSecret =
process.env.HASURA_GRAPHQL_ADMIN_SECRET ??
process.env.HASURA_ADMIN_SECRET ??
process.env.NEXT_PUBLIC_HASURA_ADMIN_SECRET
return { url, adminSecret }
}
function getHasuraConfig() {
const url =
process.env.HASURA_GRAPHQL_ENDPOINT
const adminSecret =
process.env.HASURA_GRAPHQL_ADMIN_SECRET ??
process.env.HASURA_ADMIN_SECRET
return { url, adminSecret }
}
🤖 Prompt for AI Agents
In `@apps/pro-web/app/actions/document-operations/log-document-operation.ts`
around lines 42 - 53, In getHasuraConfig, remove the NEXT_PUBLIC fallback for
the admin secret: stop using process.env.NEXT_PUBLIC_HASURA_ADMIN_SECRET and
only read server-side variables (process.env.HASURA_GRAPHQL_ADMIN_SECRET and
process.env.HASURA_ADMIN_SECRET) so the adminSecret cannot be exposed to the
client; update the adminSecret assignment in getHasuraConfig accordingly and
ensure the url fallback to NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT is left only if
intentionally needed for runtime, otherwise use the server-only
HASURA_GRAPHQL_ENDPOINT.

Comment on lines 246 to 254
const versionAfter =
typeof (newDocument as any)?.currentVersion === 'number'
? (newDocument as any).currentVersion
: null

const checksumAfter =
(newDocument as any)?.versions?.find?.(
(v: any) => v?.version === versionAfter,
)?.checksum ?? 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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, check if the file exists and get basic info
fd "use-workspace-chat.tsx" apps/pro-web/lib/hooks/

Repository: bitcashorg/masterbots

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Read the file and show lines around 246-254 with context
cat -n apps/pro-web/lib/hooks/use-workspace-chat.tsx | sed -n '230,270p'

Repository: bitcashorg/masterbots

Length of output: 1396


🏁 Script executed:

#!/bin/bash
# Search for type definitions related to documentData and newDocument
rg -n "documentData|newDocument" apps/pro-web/lib/hooks/use-workspace-chat.tsx | head -30

Repository: bitcashorg/masterbots

Length of output: 1066


🏁 Script executed:

#!/bin/bash
# Get more context around line 186 to see where documentData comes from
cat -n apps/pro-web/lib/hooks/use-workspace-chat.tsx | sed -n '180,195p'

Repository: bitcashorg/masterbots

Length of output: 604


🏁 Script executed:

#!/bin/bash
# Get more context around line 218 to see where newDocument comes from
cat -n apps/pro-web/lib/hooks/use-workspace-chat.tsx | sed -n '210,230p'

Repository: bitcashorg/masterbots

Length of output: 813


🏁 Script executed:

#!/bin/bash
# Search for the mutation/function that returns newDocument
rg -n "newDocument" apps/pro-web/lib/hooks/use-workspace-chat.tsx -B3 | head -50

Repository: bitcashorg/masterbots

Length of output: 1082


🏁 Script executed:

#!/bin/bash
# Check imports and type definitions at the beginning of the file
head -100 apps/pro-web/lib/hooks/use-workspace-chat.tsx

Repository: bitcashorg/masterbots

Length of output: 3105


🏁 Script executed:

#!/bin/bash
# Find the lib/queries files
fd -t f -e ts -e tsx lib/queries

Repository: bitcashorg/masterbots

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Search for useDocumentContent and useUpdateDocumentContent definitions
rg -n "useDocumentContent|useUpdateDocumentContent" lib/queries -A10 | head -80

Repository: bitcashorg/masterbots

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Look for type definitions related to document structure
rg -n "currentVersion|versions" lib/queries -B2 -A2 | head -100

Repository: bitcashorg/masterbots

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Find where lib/queries actually is
fd -t d queries apps/

Repository: bitcashorg/masterbots

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Search more broadly for useDocumentContent definition
rg -n "export.*useDocumentContent|function useDocumentContent" --type ts --type tsx

Repository: bitcashorg/masterbots

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Check the structure of apps directory
ls -la apps/

Repository: bitcashorg/masterbots

Length of output: 336


🏁 Script executed:

#!/bin/bash
# Search for useDocumentContent definition in the correct location
rg -n "export.*useDocumentContent|function useDocumentContent" apps/pro-web/lib/queries/ -A10

Repository: bitcashorg/masterbots

Length of output: 1031


🏁 Script executed:

#!/bin/bash
# List files in apps/pro-web/lib/queries/
ls -la apps/pro-web/lib/queries/

Repository: bitcashorg/masterbots

Length of output: 754


🏁 Script executed:

#!/bin/bash
# Search for useUpdateDocumentContent
rg -n "useUpdateDocumentContent" apps/pro-web/lib/queries/ -A10

Repository: bitcashorg/masterbots

Length of output: 10048


🏁 Script executed:

#!/bin/bash
# Search for type definitions with currentVersion
rg -n "currentVersion" apps/pro-web/lib/queries/ -B5 -A5

Repository: bitcashorg/masterbots

Length of output: 14775


🏁 Script executed:

#!/bin/bash
# Find the WorkspaceDocumentMetadata type definition
rg -n "type WorkspaceDocumentMetadata|interface WorkspaceDocumentMetadata" -A30

Repository: bitcashorg/masterbots

Length of output: 1953


🏁 Script executed:

#!/bin/bash
# Check what useUpdateDocumentContent returns by looking at the return statement
cat -n apps/pro-web/lib/queries/use-document-content.ts | sed -n '325,335p'

Repository: bitcashorg/masterbots

Length of output: 553


🏁 Script executed:

#!/bin/bash
# Search for any type definitions or imports related to WorkspaceDocumentMetadata
rg -n "WorkspaceDocumentMetadata" apps/pro-web/lib/queries/ -B2 -A5 | head -100

Repository: bitcashorg/masterbots

Length of output: 7432


Remove unnecessary as any casts—the type is already properly defined.

The newDocument object returned from useUpdateDocumentContent is already typed as WorkspaceDocumentMetadata, which explicitly defines both currentVersion and versions properties. The as any casts at lines 247–248 and 252 bypass TypeScript's type safety unnecessarily:

// Remove the `as any` casts:
const versionAfter =
    typeof newDocument?.currentVersion === 'number'
        ? newDocument.currentVersion
        : null

const checksumAfter =
    newDocument?.versions?.find?.(
        (v: WorkspaceDocumentVersion) => v?.version === versionAfter,
    )?.checksum ?? null

Update the type hint for the find callback parameter to use the proper WorkspaceDocumentVersion type instead of any.

🤖 Prompt for AI Agents
In `@apps/pro-web/lib/hooks/use-workspace-chat.tsx` around lines 246 - 254, Remove
the unnecessary "as any" casts on newDocument: use the typed properties directly
(newDocument?.currentVersion and newDocument?.versions). Compute versionAfter by
checking typeof newDocument?.currentVersion === 'number' and assign
newDocument.currentVersion or null. For checksumAfter, call
newDocument?.versions?.find with the callback parameter typed as
WorkspaceDocumentVersion (e.g., (v: WorkspaceDocumentVersion) => v?.version ===
versionAfter) and read ?.checksum ?? null. This change affects the newDocument,
versionAfter and checksumAfter expressions created after the
useUpdateDocumentContent result.

Comment on lines 959 to 963
finalUserPrompt = followingQuestionsPrompt(
userPrompt,
scopedHistory as unknown as any, // matches existing pattern in use-mb-chat.tsx
'',
)
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

🧩 Analysis chain

🏁 Script executed:

# First, let's see the context around the flagged lines in use-workspace-chat.tsx
cat -n apps/pro-web/lib/hooks/use-workspace-chat.tsx | sed -n '950,970p'

Repository: bitcashorg/masterbots

Length of output: 793


🏁 Script executed:

# Find the definition/declaration of followingQuestionsPrompt
rg -n 'followingQuestionsPrompt' --type=ts --type=tsx -B2 -A8

Repository: bitcashorg/masterbots

Length of output: 92


🏁 Script executed:

# Check use-mb-chat.tsx for the mentioned pattern
rg -n 'followingQuestionsPrompt' apps/pro-web/lib/hooks/use-mb-chat.tsx -B2 -A5

Repository: bitcashorg/masterbots

Length of output: 570


🏁 Script executed:

# Find the definition of followingQuestionsPrompt in the prompts file
rg -n 'export.*followingQuestionsPrompt' -A 15

Repository: bitcashorg/masterbots

Length of output: 2844


🏁 Script executed:

# Get more context from use-mb-chat.tsx around the followingQuestionsPrompt call
cat -n apps/pro-web/lib/hooks/use-mb-chat.tsx | sed -n '1450,1465p'

Repository: bitcashorg/masterbots

Length of output: 539


🏁 Script executed:

# Now let's look at how scopedHistory is constructed in use-workspace-chat.tsx
cat -n apps/pro-web/lib/hooks/use-workspace-chat.tsx | sed -n '930,965p'

Repository: bitcashorg/masterbots

Length of output: 1289


🏁 Script executed:

# Let's check MessageDB type to understand what's expected
rg -n 'type MessageDB|interface MessageDB' -A 10

Repository: bitcashorg/masterbots

Length of output: 47


🏁 Script executed:

# Search more broadly for MessageDB definition
rg 'MessageDB' -B2 -A6 | head -50

Repository: bitcashorg/masterbots

Length of output: 3705


🏁 Script executed:

# Check what types are imported/used in use-workspace-chat.tsx
head -50 apps/pro-web/lib/hooks/use-workspace-chat.tsx | grep -E 'import|from'

Repository: bitcashorg/masterbots

Length of output: 1380


Replace the double cast with proper typing of scopedHistory to match the expected MessageDB structure.

The cast scopedHistory as unknown as any bypasses type safety and masks a real type mismatch. The array is constructed with role, content, messageId, and createdAt properties, but followingQuestionsPrompt expects Array<MessageDB & { id?: string }>. While this pattern appears elsewhere in use-mb-chat.tsx, it indicates a systemic issue rather than justifying the workaround. Construct scopedHistory to conform to the expected type, or create a proper transformation function that maps the shape correctly.

🤖 Prompt for AI Agents
In `@apps/pro-web/lib/hooks/use-workspace-chat.tsx` around lines 959 - 963, The
double-cast bypass in the call to followingQuestionsPrompt should be replaced by
constructing scopedHistory with the correct MessageDB shape (Array<MessageDB & {
id?: string }>) instead of using "scopedHistory as unknown as any"; locate where
scopedHistory is built in use-workspace-chat.tsx and map each item to include
the MessageDB fields (role, content, messageId -> id if needed, createdAt) or
write a small transform function (e.g., mapScopedToMessageDB) that converts the
existing objects to MessageDB objects, then pass the transformed array into
followingQuestionsPrompt so TypeScript types line up without unsafe casts
(mirror the correct typing pattern from use-mb-chat.tsx but implement a proper
mapping rather than double casting).

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: 2

🤖 Fix all issues with AI agents
In `@apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx`:
- Around line 324-327: The Templates Button renders but has no behavior; either
wire it to the template flow or mark it as a placeholder—add a clear disabled
state and tooltip/title instead of leaving it clickable. Update the Button (the
one rendering LayoutTemplate and "Templates") to include disabled and a title
like "Coming soon" (or add an onClick that opens your template picker/modal if
you want real behavior), and ensure the visual state still uses
variant="outline" and size="sm" so the UI remains consistent; reference the
Button component rendering LayoutTemplate to locate where to apply the change.
- Around line 330-368: The raw vs structured state desync is caused because
editing the raw textarea (input) never updates structuredData; update the
mode-switch logic so that when viewMode changes from 'raw' to 'structured' you
attempt to parse the current input and setStructuredData accordingly (e.g.,
parse JSON/YAML or a simple delimiter format), falling back to a safe default
(empty fields) or surfacing a validation error if parsing fails; ensure you
update structuredData via setStructuredData and preserve input via setInput, and
keep the existing handleStructuredChange logic for structured edits so changes
remain bidirectionally consistent between input and structuredData.
♻️ Duplicate comments (1)
apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx (1)

68-85: Same issues apply as noted in the web app version.

The suffix replacement fragility, state desync between modes, non-functional Templates button, and inline arrow functions noted in apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx apply equally here. Consolidating to a shared component would address all instances at once.

🧹 Nitpick comments (5)
apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx (4)

68-85: Potential bug: suffix removal may fail if suffix contains regex metacharacters.

String.prototype.replace with a string pattern only removes the first occurrence and doesn't escape regex metacharacters. If a suffix ever contains characters like . or ?, this could behave unexpectedly. Currently the suffixes are safe, but this is fragile.

♻️ Safer replacement approach
 if (isActive) {
   newOptions = activeOptions.filter((id) => id !== optionId)
-  newInput = newInput.replace(suffix, '')
+  // Split and rejoin to avoid regex edge cases
+  const suffixIndex = newInput.indexOf(suffix)
+  if (suffixIndex !== -1) {
+    newInput = newInput.slice(0, suffixIndex) + newInput.slice(suffixIndex + suffix.length)
+  }
 } else {

87-190: Consider memoizing getStructureFields for render stability.

getStructureFields() is called on every render, creating new array/object references. While the performance impact is minimal, memoizing fields would prevent unnecessary re-renders of child components that depend on referential equality.

♻️ Memoization suggestion
+const fields = React.useMemo(() => {
   // Model Detection
   const isO1 = selectedModel.match(/(o1|gpt-5)/i)
   const isClaude = selectedModel.match(/(claude|anthropic)/i)
   const isGemini = selectedModel.match(/(gemini)/i)
-
-  // Determine Structure Fields
-  const getStructureFields = () => {
     if (isO1) {
       return [
         // ... existing fields
       ]
     }
     // ... rest of logic
-  }
-
-  const fields = getStructureFields()
+}, [selectedModel])

301-322: Use stable function references for view mode toggles.

As per coding guidelines, prefer regular function references over inline arrow functions in JSX to avoid extra re-renders.

♻️ Extract handlers
+const handleSetRawMode = React.useCallback(() => setViewMode('raw'), [])
+const handleSetStructuredMode = React.useCallback(() => setViewMode('structured'), [])

 // In JSX:
 <button
-  onClick={() => setViewMode('raw')}
+  onClick={handleSetRawMode}
   // ...
 >
   Raw
 </button>
 <button
-  onClick={() => setViewMode('structured')}
+  onClick={handleSetStructuredMode}
   // ...
 >
   Structured
 </button>

301-311: Add explicit type="button" to prevent form submission edge cases.

Plain <button> elements default to type="submit". While these are in a Popover (not a form), adding explicit type="button" improves robustness if the DOM structure changes.

 <button
+  type="button"
   onClick={() => setViewMode('raw')}
   // ...
 >
apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx (1)

1-375: Extract PromptEnhancer to a shared package to eliminate duplication.

This component is identically duplicated between apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx and apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx. Any bug fix or feature change requires updates in both locations, creating maintenance burden.

Move PromptEnhancer to a shared package (e.g., packages/ui or packages/shared-components) and import it from both apps. This aligns with DRY principles and ensures consistent behavior across environments.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a9dad8 and b4c1509.

📒 Files selected for processing (4)
  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
  • apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/web/components/routes/chat/prompt-form/index.tsx
  • apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/components/routes/chat/prompt-form/index.tsx
🧰 Additional context used
📓 Path-based instructions (7)
apps/{web,pro-web}/components/routes/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Place page-specific components under components/routes/[feature]/

Organize page-specific components under components/routes/

Files:

  • apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
apps/{web,pro-web}/**/*.tsx

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

apps/{web,pro-web}/**/*.tsx: Declare React components with the function keyword
Name React components using PascalCase
Order component files: imports → constants → component → styled components → types/interfaces
Use functional components and hooks; type props with a TypeScript interface
Use regular function references (not inline arrows) for React event handlers to avoid extra re-renders

apps/{web,pro-web}/**/*.tsx: Use regular function references for event handlers instead of inline arrow functions in JSX
Declare React components with the function keyword (function ComponentName() {})
Use PascalCase for React component names
Order React component files: imports, constants, component, styled components, types/interfaces
Style components with Tailwind CSS utility classes

Files:

  • apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Avoid default exports; prefer named exports
Use meaningful variable names; prefix booleans with is/has/does/should
Use interface for objects/classes; type for unions/tuples/aliases; enum for fixed sets; const for literals
Avoid any; prefer specific types
Leverage type inference where clear; annotate function params and return types
Prefer type assertions with 'as' over angle-bracket syntax
Use type guards to narrow types in conditionals

**/*.{ts,tsx}: Avoid default exports; prefer named exports
Use modern JavaScript features with TypeScript across the codebase
Use meaningful names; prefix booleans with is/has/does/should (e.g., isDisabled)
Receive an object and return an object (RORO) for functions interfacing with external services
Type definitions: use interface for objects/classes; type for unions/tuples/aliases; const for literals; enum for fixed sets
Avoid any; prefer specific types
Leverage type inference; omit explicit annotations when obvious
Add explicit type annotations for function parameters and return values
Prefer type assertions with as over angle-bracket syntax
Use type guards to narrow types within conditionals
Favor composition over inheritance in code structure

**/*.{ts,tsx}: Avoid default exports; prefer named exports
Prefer RORO (Receive an object, return an object) for function signatures, especially for external service interactions
Use interface for object shapes/classes; type for unions/tuples/aliases; const for literals; enum for fixed enumerations
Avoid using any; prefer precise types
Leverage type inference when clear; omit redundant annotations
Explicitly annotate function parameters and return types
Prefer type assertions with as over angle-bracket syntax
Use type guards to narrow types safely
Use meaningful variable names; prefix booleans with is/has/does/should
Favor composition over inheritance
Use lowercase dash-separated names for files and folders; use extensions like .config.ts, .test.ts, .context.tsx, .typ...

Files:

  • apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
apps/{web,pro-web}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

apps/{web,pro-web}/**/*.{ts,tsx}: Use lowercase kebab-case for directories and filenames (components can omit extension)
Verify DOM operations and consider timing (RAF/delays) when manipulating the DOM in React
Use Tailwind CSS utility classes for styling React components

apps/{web,pro-web}/**/*.{ts,tsx}: Use TypeScript to define React props types (interfaces for props)
Use custom hooks (useMBChat, useThread, useSidebar, useModel) for domain-specific state and keep providers focused and composed hierarchically
Separate presentation from business logic and compose small components
Use hasura.service.ts for all GraphQL operations from UI layers

apps/{web,pro-web}/**/*.{ts,tsx}: Prefer Server Components; use the use client directive only when client-side features are required
Implement Nested Layouts using the App Router
Use Streaming and Suspense in Next.js 14/15 where appropriate
Follow Next.js file conventions as per documentation
Use IndexedDB for local caching to enable immediate UI updates

Files:

  • apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Use lowercase with dash-separated directory and file names; use specific extensions: .config.ts, .test.ts, .context.tsx, .type.ts, .hook.ts; components may omit extension
Use Bun for package management and scripts instead of npm/yarn
Use Biome for formatting and linting via bun format-and-lint:fix; avoid Prettier/ESLint configs

Files:

  • apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

**/*.tsx: Avoid inline arrow functions in JSX props; use stable function references for event handlers
Declare React components with the function keyword (functional components)
Use PascalCase for React component names
Use functional components and hooks instead of class components
Type component props with TypeScript interfaces

Files:

  • apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
apps/{web,pro-web}/components/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Order component files: imports → constants → component → styled components → types/interfaces

Files:

  • apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
🧠 Learnings (8)
📓 Common learnings
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.
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.
📚 Learning: 2025-02-17T16:51:19.102Z
Learnt from: Bran18
Repo: bitcashorg/masterbots PR: 376
File: apps/masterbots.ai/components/routes/chat/chat.tsx:92-102
Timestamp: 2025-02-17T16:51:19.102Z
Learning: For the Chat component in apps/masterbots.ai/components/routes/chat/chat.tsx, the developer prefers to verify potential optimizations through local testing before accepting suggestions, particularly for performance-related changes like useCallback usage in debounce implementations.

Applied to files:

  • apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to apps/{web,pro-web}/**/*.tsx : Use functional components and hooks; type props with a TypeScript interface

Applied to files:

  • apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx
  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to apps/{web,pro-web}/**/*.{ts,tsx} : Use TypeScript to define React props types (interfaces for props)

Applied to files:

  • apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to apps/{web,pro-web}/**/*.{ts,tsx} : Use custom hooks (useMBChat, useThread, useSidebar, useModel) for domain-specific state and keep providers focused and composed hierarchically

Applied to files:

  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
📚 Learning: 2024-11-07T14:40:39.595Z
Learnt from: Bran18
Repo: bitcashorg/masterbots PR: 301
File: apps/masterbots.ai/components/routes/chat/chat-combobox.tsx:33-33
Timestamp: 2024-11-07T14:40:39.595Z
Learning: In `apps/masterbots.ai/components/routes/chat/chat-combobox.tsx`, the 'MB' logo is temporarily represented by the string `'MB'` until the custom MB icon is created.

Applied to files:

  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
📚 Learning: 2025-10-24T20:40:56.510Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 573
File: apps/pro-web/components/routes/pro/pro.tsx:49-49
Timestamp: 2025-10-24T20:40:56.510Z
Learning: In apps/pro-web/lib/hooks/use-mb-chat.tsx, the useMBChat() hook returns a tuple [MBChatHookState, MBChatHookActions], not a direct object. Use array destructuring to access state (index 0) and actions (index 1), e.g., const [state, actions] = useMBChat() or const [{ isLoading }] = useMBChat().

Applied to files:

  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
📚 Learning: 2024-12-05T01:08:50.085Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2024-12-05T01:08:50.085Z
Learning: The frontend uses shadcn/ui components and follows a strict component organization with proper separation of client/server components in Next.js.

Applied to files:

  • apps/pro-web/components/routes/chat/prompt-form/index.tsx
🧬 Code graph analysis (2)
apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx (1)
apps/pro-web/lib/utils.ts (1)
  • cn (9-11)
apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx (1)
apps/pro-web/lib/utils.ts (1)
  • cn (9-11)
🔇 Additional comments (4)
apps/pro-web/components/routes/chat/prompt-form/index.tsx (4)

247-251: Good SSR/CSR-stable ID generation.

Using useId() instead of nanoid() ensures deterministic IDs across server and client renders, preventing hydration mismatches. The colon normalization is a sensible safety measure.


481-487: PromptEnhancer integration looks correct.

The component receives the necessary props (input, setInput, selectedModel) and is positioned alongside the attachment controls. The selectedModel from useModel() hook enables model-aware structured prompts.


488-598: Attachment Popover structure is well-organized.

The Command-based UI with accordion for saved attachments and grouped actions for new attachments follows existing patterns. The conditional rendering based on userHasRelatedAttachment provides good UX by hiding empty sections.


97-106: This concern is unfounded—the fixed ID is handled correctly by conditional logic.

The 'thread-context-default' ID is used only when adding an attachment named exactly 'Thread Context.txt'. If a file with that name already exists, the next one is created with name 'Thread Context (1).txt' and assigned a unique nanoid(16) ID instead. The uniqBy([...attachments, attachment], 'id') deduplication in addAttachmentObject (line 148, use-chat-attachments.ts) prevents duplicate IDs within the attachment list. Additionally, only one PromptForm instance exists in the codebase, eliminating cross-instance collision risk.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +324 to +327
<Button variant="outline" size="sm" className="gap-2">
<LayoutTemplate className="size-4" />
Templates
</Button>
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

Templates button has no functionality.

The button renders but has no onClick handler. If this is intentional placeholder, consider adding a disabled prop or tooltip indicating "Coming soon".

Do you want me to help implement a templates system or add a placeholder UI indicating this feature is coming?

🤖 Prompt for AI Agents
In `@apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx` around lines
324 - 327, The Templates Button renders but has no behavior; either wire it to
the template flow or mark it as a placeholder—add a clear disabled state and
tooltip/title instead of leaving it clickable. Update the Button (the one
rendering LayoutTemplate and "Templates") to include disabled and a title like
"Coming soon" (or add an onClick that opens your template picker/modal if you
want real behavior), and ensure the visual state still uses variant="outline"
and size="sm" so the UI remains consistent; reference the Button component
rendering LayoutTemplate to locate where to apply the change.

Comment on lines +330 to +368
{/* Main Input Area */}
<div className="min-h-[200px] max-h-[60vh] overflow-y-auto pr-1">
{viewMode === 'raw' ? (
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
className="w-full h-[300px] resize-none font-mono text-sm"
placeholder="Enter your prompt here..."
/>
) : (
<div className="space-y-4">
{fields.map((field) => (
<div key={field.key} className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
{field.label}
</Label>
{field.type === 'textarea' ? (
<Textarea
placeholder={field.placeholder}
value={structuredData[field.key] || ''}
onChange={(e) =>
handleStructuredChange(field.key, e.target.value)
}
className="resize-y min-h-[80px]"
/>
) : (
<Input
placeholder={field.placeholder}
value={structuredData[field.key] || ''}
onChange={(e) =>
handleStructuredChange(field.key, e.target.value)
}
/>
)}
</div>
))}
</div>
)}
</div>
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

State desync between Raw and Structured modes.

When users edit in Raw mode, structuredData is not updated. Switching to Structured mode shows stale values from the last structured edit, not the current raw input. This could confuse users who expect bidirectional sync.

Consider either:

  1. Parsing the raw input back into structured fields when switching modes
  2. Clearing structured fields when raw input is manually edited
  3. Disabling mode switching after manual raw edits (with a warning)
🤖 Prompt for AI Agents
In `@apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx` around lines
330 - 368, The raw vs structured state desync is caused because editing the raw
textarea (input) never updates structuredData; update the mode-switch logic so
that when viewMode changes from 'raw' to 'structured' you attempt to parse the
current input and setStructuredData accordingly (e.g., parse JSON/YAML or a
simple delimiter format), falling back to a safe default (empty fields) or
surfacing a validation error if parsing fails; ensure you update structuredData
via setStructuredData and preserve input via setInput, and keep the existing
handleStructuredChange logic for structured edits so changes remain
bidirectionally consistent between input and structuredData.

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: 6

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/services/hasura/hasura.service.ts (1)

680-742: Retry logic silently swallows errors and still throws original error.

The fail-closed retry catches retryErr but only logs it, then falls through to throw the original error. If the retry succeeds, this works fine. But if the retry fails, the original error message (about scopeTitleSnapshot) is thrown instead of the actual retry failure, which can be misleading for debugging.

Also, the canonicalDocNodeId is computed but never included in the retry's rest object—if the original insert intended to include docNodeId, the retry will omit it.

🐛 Suggested improvement
 		if (
 			msg.includes('scopeTitleSnapshot') ||
 			msg.includes('scope_title_snapshot')
 		) {
 			try {
 				const obj = object as Record<string, unknown>
 				const {
 					scopeTitleSnapshot: _a,
 					scope_title_snapshot: _b,
 					...rest
 				} = obj
+				// Preserve docNodeId if it was intended for the original insert
+				const retryObject = canonicalDocNodeId == null
+					? rest
+					: { ...rest, docNodeId: canonicalDocNodeId }

 				const { insertMessageOne: retryMessage } = await client.mutation({
 					insertMessageOne: {
-						__args: { object: rest },
+						__args: { object: retryObject },
 						__scalar: true,
 					},
 				})

 				return retryMessage as Message
 			} catch (retryErr) {
 				console.error('Error saving new message (retry failed):', retryErr)
+				throw new Error('Failed to save new message (retry also failed).')
 			}
 		}

 		console.error('Error saving new message:', error)
 		throw new Error('Failed to save new message.')
🤖 Fix all issues with AI agents
In `@apps/pro-web/lib/url.ts`:
- Around line 86-89: The filter building wordMatches uses user-controlled
domainSlug directly in new RegExp which can cause ReDoS; escape regex
metacharacters in domainSlug before constructing regex (or avoid RegExp entirely
by using a safe string comparison like slug === domainSlug or
slug.split/startsWith depending on intent). Update the code around
domainSlugs.filter and the regex creation to call a safe escape helper (e.g.,
escapeRegex(domainSlug)) or replace the regex branch with a non-regex comparison
to safely match word boundaries.

In `@apps/pro-web/lib/workspace-debug.ts`:
- Around line 22-27: DEBUG_FLAGS is computed once at module load so toggling
window.__DEBUG_* at runtime has no effect; change it to evaluate dynamically by
replacing the static boolean values with runtime getters or functions (e.g.,
convert DEBUG_FLAGS into an object with getters for
workspace/content/chat/markdown or export a getDebugFlags() function) so each
access reads w?.__DEBUG_WORKSPACE__, w?.__DEBUG_WORKSPACE_CONTENT__,
w?.__DEBUG_WORKSPACE_CHAT__, and w?.__DEBUG_MARKDOWN__ anew; update any call
sites that expect the previous static object if necessary.

In `@apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx`:
- Around line 1-377: This duplicates the pro-web PromptEnhancer — extract
PromptEnhancer (and constants like ENHANCEMENT_OPTIONS) into a shared package
(e.g., packages/shared or packages/ui) and import it here to avoid maintenance
drift; while migrating, fix the technical feedback: change model detection to
boolean checks using RegExp.test() for isO1/isClaude/isGemini instead of
.match(), memoize computed fields by wrapping getStructureFields()/fields in
React.useMemo depending on selectedModel and those boolean flags, and replace
inline arrow callbacks in JSX with stable useCallback handlers (e.g., replace
inline onCheckedChange, onClick for view toggles, onChange for Textarea/Input,
Popover onOpenChange and expand toggle) using named functions like
handleOptionToggle, handleStructuredChange, buildPromptFromStructure to keep
stable references and avoid re-renders.

In `@packages/mb-drizzle/src/drizzle/schema.ts`:
- Around line 566-582: Indexes document_operations_doc_node_scope_created_idx
and document_operations_document_key_created_idx use the wrong operator class
for text columns; update the op calls for table.docNodeId, table.scopeType,
table.scopeId, and table.documentKey from 'timestamptz_ops' to 'text_ops' (leave
the section index as-is since it already uses text_ops); modify the index
definitions referencing those symbols so the text columns use op('text_ops')
consistently.
- Around line 492-524: Indexes in packages/mb-drizzle/src/drizzle/schema.ts use
incorrect operator classes causing Postgres to ignore them; update the .op(...)
calls to match each column type: for
index('assistant_answers_thread_doc_scope_idx') change
table.threadId.op('text_ops') -> 'uuid_ops'; for
index('assistant_answers_thread_doc_scope_turn_idx') change
table.threadId.op('int4_ops') -> 'uuid_ops', table.scopeId.op('int4_ops') ->
'text_ops', table.turnIndex.op('enum_ops') -> 'int4_ops'; for
index('assistant_answers_thread_scope_idx') change table.threadId.op('enum_ops')
-> 'uuid_ops'; for index('assistant_answers_thread_scope_turn_idx') change
table.scopeId.op('enum_ops') -> 'text_ops' and table.turnIndex.op('uuid_ops') ->
'int4_ops'; ensure every .op(...) matches the actual column type (uuid ->
uuid_ops, text -> text_ops, integer -> int4_ops, enum -> enum_ops).
- Around line 906-946: The new message table index definitions (index names
message_doc_node_id_idx, message_thread_doc_created_idx,
message_thread_doc_scope_created_idx, message_thread_scope_created_at_idx,
message_thread_scope_qchain_idx, message_thread_scope_recent_idx) use incorrect
operator classes for several columns; update the .op(...) calls on the
referenced columns (table.docNodeId, table.createdAt, table.threadId,
table.scopeType, table.scopeId) to match their actual column types: use text_ops
for text columns (docNodeId, scopeId), uuid_ops for uuid columns (threadId),
timestamptz_ops for timestamp/timestamptz columns (createdAt), and enum_ops for
enum columns (scopeType); ensure each index's .op(...) entries are corrected
accordingly in the index(...) .using(...) definitions where those columns
appear.
♻️ Duplicate comments (12)
apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx (2)

326-329: Templates button has no functionality.

The button renders but lacks an onClick handler. Add disabled with a tooltip or wire up the template system.


332-370: State desync between Raw and Structured modes.

Editing in Raw mode doesn't update structuredData. Switching to Structured shows stale values. Consider clearing structured fields when raw input is edited, or parse raw input when switching modes.

apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx (2)

326-329: Templates button has no functionality.

Same issue as flagged in past review - button needs onClick handler or disabled state.


332-370: State desync between Raw and Structured modes.

Same issue as flagged in past review - bidirectional sync needed or mode-switch handling.

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

976-982: The scopedHistory as any cast bypasses type safety.

The array is constructed with role, content, messageId, and createdAt properties, but followingQuestionsPrompt expects Array<MessageDB & { id?: string }>. This was flagged in a previous review. Consider creating a proper mapping function or adjusting the type definitions to align.

apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts (1)

155-173: Non-schema errors are swallowed without logging.

When the initial insert fails and the error message doesn't match the schema lag conditions, the function returns silently at line 172, losing the original error context. This makes debugging non-schema-related failures difficult. Consider adding a console.warn for non-schema errors while maintaining the fail-closed behavior.

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

486-503: Fallback URL may produce malformed paths with empty segments.

When threadQuestionSlug is missing and fallback kicks in, if other params (like usernameSlug, category, etc.) are also empty, the URL could contain empty segments (e.g., /u//t///). Consider routing through profilesThreadUrl for the user case similar to how threadQuestionUrl now uses threadUrl.

🐛 Suggested defensive approach
 			// If missing, fall back to the parent profile thread URL (no error).
 			if (!threadQuestionSlug) {
 				if (type === 'chatbot') {
+					if (!chatbot || !threadSlug) return '/'
 					return ['', 'b', chatbot?.toLowerCase() ?? '', threadSlug ?? ''].join(
 						'/',
 					)
 				}
-				return [
-					'',
-					'u',
-					usernameSlug ?? '',
-					't',
-					normalizeCategorySlug(category ?? ''),
-					normalizeDomainSlug(domain ?? '', raw),
-					chatbot?.toLowerCase() ?? '',
-					threadSlug ?? '',
-				].join('/')
+				// Route through profilesThreadUrl for consistent validation
+				return urlBuilders.profilesThreadUrl({
+					type: 'user',
+					usernameSlug: usernameSlug ?? '',
+					category: category ?? '',
+					domain: domain ?? '',
+					chatbot: chatbot ?? '',
+					threadSlug: threadSlug ?? '',
+					raw,
+				})
 			}
apps/pro-web/components/shared/shared-accordion.tsx (1)

379-379: Potential runtime error: accessing categories[0] without null check.

The expression thread?.chatbot?.categories[0]?.category?.name will throw if categories is an empty array since categories[0] returns undefined, but the preceding ?. does not guard array access.

🐛 Proposed fix
-		const category = thread?.chatbot?.categories[0]?.category?.name
+		const category = thread?.chatbot?.categories?.[0]?.category?.name
apps/pro-web/components/routes/chat/chat-list/index.tsx (1)

109-114: Double type assertion bypasses type safety.

The as unknown as ThreadMessage[] double-cast pattern masks potential type mismatches. Consider creating a normalization function with runtime validation.

🔧 Suggested improvement
function normalizeMessages(raw: unknown[]): ThreadMessage[] {
  return (raw ?? []).map(m => {
    const msg = m as Record<string, unknown>
    return {
      messageId: (msg.messageId ?? msg.id ?? '') as string,
      content: (msg.content ?? '') as string,
      role: (msg.role ?? 'user') as string,
      createdAt: msg.createdAt as string | undefined,
      // ... other required fields
    } as ThreadMessage
  })
}

// Usage:
const chatMessages = normalizeMessages(messages || activeThread?.messages || [])
apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts (3)

81-84: Inconsistent token estimation with codebase.

This estimateTokens function returns 0 for empty strings, while apps/pro-web/lib/simple-rag/prompt.ts uses Math.max(1, Math.ceil(text.length / 4)) which clamps to minimum 1. This inconsistency could cause division-by-zero or unexpected behavior in token budget calculations.

🔧 Suggested fix
 function estimateTokens(text: string): number {
 	// deterministic + cheap (matches the guide's intent)
-	return Math.ceil((text ?? '').length / 4)
+	return Math.max(1, Math.ceil((text ?? '').length / 4))
 }

Or import from the shared location to maintain consistency.


129-140: Sanitize regex input to prevent potential ReDoS.

The tryExtractExpectedType function constructs a regex from varName which could contain special characters. While the risk is low since varName comes from internal code, sanitizing the input eliminates the concern flagged by static analysis.

🔧 Suggested fix
 function tryExtractExpectedType(
 	errMsg: string,
 	varName: string,
 ): string | null {
+	// Sanitize varName to prevent ReDoS
+	const safeVarName = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
 	const re = new RegExp(
-		`\\$${varName}" of type "[^"]+" used in position expecting type "([^"]+)"`,
+		`\\$${safeVarName}" of type "[^"]+" used in position expecting type "([^"]+)"`,
 	)
 	const m = errMsg.match(re)
 	return m?.[1] ?? null
 }

371-378: Remove unsafe double type assertion.

The as unknown as AssistantAnswerMemory[] cast bypasses type safety. The mapped object already has the correct shape, so the cast is unnecessary.

🔧 Suggested fix
 const normalize = (rows: SearchRow[]): AssistantAnswerMemory[] =>
 	(rows ?? []).map((r) => ({
 		messageId: r.message_id,
 		turnIndex: r.turn_index,
 		content: r.content,
 		cosine: r.cosine,
 		createdAt: r.created_at,
-	})) as unknown as AssistantAnswerMemory[]
+	}))
🧹 Nitpick comments (13)
apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx (3)

87-90: Model detection flags should be boolean, not RegExpMatchArray | null.

Using .match() returns a match array or null. While this works for truthiness checks, it leads to imprecise types. Convert to explicit booleans for clarity and type safety.

♻️ Suggested fix
-	const isO1 = selectedModel.match(/(o1|gpt-5)/i)
-	const isClaude = selectedModel.match(/(claude|anthropic)/i)
-	const isGemini = selectedModel.match(/(gemini)/i)
+	const isO1 = /(o1|gpt-5)/i.test(selectedModel)
+	const isClaude = /(claude|anthropic)/i.test(selectedModel)
+	const isGemini = /gemini/i.test(selectedModel)

93-190: Memoize getStructureFields to avoid recreation on every render.

The function and fields assignment are recreated on each render. Since they only depend on the model detection flags, wrap in useMemo.

♻️ Suggested refactor
-	// Determine Structure Fields
-	const getStructureFields = () => {
-		if (isO1) {
-			return [
-				// ... fields
-			]
-		}
-		// ... rest of conditions
-	}
-
-	const fields = getStructureFields()
+	// Determine Structure Fields
+	const fields = React.useMemo(() => {
+		if (isO1) {
+			return [
+				{ key: 'goal', label: 'Goal', type: 'input', placeholder: 'I want...' },
+				{ key: 'returnFormat', label: 'Return Format', type: 'input', placeholder: 'Structure as...' },
+				{ key: 'constraints', label: 'Warnings & Constraints', type: 'textarea', placeholder: '- Do NOT...' },
+				{ key: 'context', label: 'Context Dump', type: 'textarea', placeholder: 'Background info...' },
+			]
+		}
+		if (isClaude) {
+			return [
+				{ key: 'taskRole', label: 'Task Context / Role Refinement', type: 'input', placeholder: 'Refine global persona (optional)...' },
+				{ key: 'context', label: 'Context', type: 'textarea', placeholder: 'Background context...' },
+				{ key: 'instruction', label: 'Instruction', type: 'textarea', placeholder: 'Analyze and perform...' },
+				{ key: 'outputFormat', label: 'Output Format', type: 'textarea', placeholder: 'Wrap in XML tags...' },
+				{ key: 'userQuery', label: 'User Query', type: 'textarea', placeholder: 'The actual request...' },
+			]
+		}
+		if (isGemini) {
+			return [
+				{ key: 'mediaData', label: 'Media & Data Description', type: 'textarea', placeholder: 'Attached file content...' },
+				{ key: 'systemInstructions', label: 'System Instructions', type: 'textarea', placeholder: 'Role and task...' },
+				{ key: 'examples', label: 'Few-Shot Examples', type: 'textarea', placeholder: 'Input: ... Output: ...' },
+				{ key: 'query', label: 'The Query', type: 'textarea', placeholder: 'Based on data above...' },
+			]
+		}
+		return [
+			{ key: 'prompt', label: 'Prompt', type: 'textarea', placeholder: 'Enter your prompt...' },
+		]
+	}, [isO1, isClaude, isGemini])

268-270: Use stable function references instead of inline arrows in JSX.

Per coding guidelines, avoid inline arrow functions in JSX props to prevent unnecessary re-renders. Extract handlers using useCallback or define stable function references.

♻️ Example for checkbox handler
+	const handleCheckboxChange = React.useCallback(
+		(optionId: string, suffix: string) => () => {
+			handleOptionToggle(optionId, suffix)
+		},
+		[handleOptionToggle]
+	)
+
 // In JSX:
-	onCheckedChange={() =>
-		handleOptionToggle(option.id, option.suffix)
-	}
+	onCheckedChange={handleCheckboxChange(option.id, option.suffix)}

Similar refactoring applies to other inline handlers at lines 285, 303, 315, 337, 352-353, and 361-362.

apps/pro-web/lib/queries/use-save-document.ts (1)

288-288: Unreachable fallback to serverDoc.content.

Since content is validated as non-empty at line 127 and ensureEmbeddedSectionIds returns the processed string, canonicalContent will always be truthy. The || serverDoc.content fallback is unreachable.

Consider simplifying for clarity:

✨ Suggested simplification
-							const rawContent = canonicalContent || serverDoc.content
+							const rawContent = canonicalContent
apps/pro-web/components/routes/pro/chat-panel-pro.tsx (3)

54-55: Consider importing ActionType from existing module.

ActionType is already defined in apps/pro-web/types/operation-meta.ts with the same union. Reusing the existing type avoids duplication and ensures consistency if the canonical type evolves.

♻️ Suggested refactor
+'use client'
+
+import type { ActionType } from '@/types/operation-meta'
 // ... other imports
-type ActionType = 'ask' | 'rewrite' | 'expand' | 'edit'
 type ScopeType = 'chat_thread' | 'doc_root' | 'heading_thread'

298-306: Unused metadata object _opMeta.

This object is computed but never used. The comment indicates it's prepared for future plumbing, but until wired up it adds cognitive overhead. Consider removing until needed or adding a TODO comment with a tracking reference.


347-362: IIFE pattern for conditional scope computation.

The immediately-invoked function expression works but is less common in React codebases. This could be simplified to a variable assignment with a conditional for improved readability.

♻️ Optional simplification
-		const scope =
-			scopeType !== 'chat_thread'
-				? (() => {
-						const fullMarkdown = currentDocumentData?.content ?? ''
-						const sections =
-							currentDocumentData?.sections ??
-							(fullMarkdown ? parseMarkdownSections(fullMarkdown) : undefined)
-
-						return {
-							scopeType,
-							scopeId,
-							docNodeId,
-							fullMarkdown,
-							sections,
-						}
-					})()
-				: undefined
+		let scope: { scopeType: ScopeType; scopeId: string | null; docNodeId: string | null; fullMarkdown: string; sections: ReturnType<typeof parseMarkdownSections> | undefined } | undefined
+
+		if (scopeType !== 'chat_thread') {
+			const fullMarkdown = currentDocumentData?.content ?? ''
+			const sections =
+				currentDocumentData?.sections ??
+				(fullMarkdown ? parseMarkdownSections(fullMarkdown) : undefined)
+
+			scope = {
+				scopeType,
+				scopeId,
+				docNodeId,
+				fullMarkdown,
+				sections,
+			}
+		}
packages/mb-drizzle/src/drizzle/schema.ts (1)

32-49: Consider consolidating duplicate enum values, but verify database intent first.

assistantAnswerScopeType (line 32) and scopeTypeEnum (line 45) define identical values: 'chat_thread', 'doc_root', 'heading_thread'. However, they map to separate PostgreSQL enum types (assistant_answer_scope_type vs scope_type_enum) and are used in different tables—the former in assistantAnswers table (line 477), the latter in message table (line 888). If these represent the same domain concept and can evolve together, consolidating to a single enum would reduce maintenance overhead. If they are intentionally separate to maintain semantic clarity between "assistant answer scope" and "message scope," the current separation is justified.

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

683-690: Avoid as any type assertions; use proper typing.

The function casts params to any which loses type safety. Consider using a properly typed destructuring or a type guard instead.

♻️ Suggested typing improvement
 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.
-	const canonicalDocNodeId: string | null = (docNodeId ??
-		doc_node_id ??
-		null) as any
+	const canonicalDocNodeId: string | null | undefined = docNodeId ?? doc_node_id ?? null
apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts (1)

522-555: Consider adding request timeout.

The hasuraGql function makes external HTTP requests without a timeout. In production, this could lead to hanging requests if Hasura becomes unresponsive.

🔧 Suggested improvement
+const HASURA_TIMEOUT_MS = 30000
+
 async function hasuraGql<TData>(
 	query: string,
 	variables?: Record<string, unknown>,
 ): Promise<TData> {
 	const endpoint =
 		process.env.HASURA_GRAPHQL_ENDPOINT ||
 		process.env.NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT
 	if (!endpoint) throw new Error('Missing HASURA_GRAPHQL_ENDPOINT')

 	const adminSecret =
 		process.env.HASURA_GRAPHQL_ADMIN_SECRET || process.env.HASURA_ADMIN_SECRET
 	if (!adminSecret) throw new Error('Missing HASURA_GRAPHQL_ADMIN_SECRET')

+	const controller = new AbortController()
+	const timeoutId = setTimeout(() => controller.abort(), HASURA_TIMEOUT_MS)
+
-	const res = await fetch(endpoint, {
+	const res = await fetch(endpoint, {
 		method: 'POST',
 		headers: {
 			'content-type': 'application/json',
 			'x-hasura-admin-secret': adminSecret,
 		},
 		body: JSON.stringify({ query, variables }),
 		cache: 'no-store',
+		signal: controller.signal,
-	})
+	}).finally(() => clearTimeout(timeoutId))
apps/pro-web/lib/hooks/use-mb-chat.tsx (2)

433-446: Remove debug console.log in production code.

The normalizeToolCall function contains a console.log statement that should be removed or gated behind a dev mode flag for production cleanliness.

🔧 Suggested 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 ?? ''

1110-1144: Duplicated mb_op_meta construction.

The enrichedChatOptions construction duplicates the mb_op_meta object structure in both branches. Consider extracting this to a shared object.

🔧 Suggested refactor
+	const mbOpMeta = {
+		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,
+	}
+
 	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,
-						},
+						mb_op_meta: mbOpMeta,
 					},
 				} 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,
-						},
+						mb_op_meta: mbOpMeta,
 					},
 				} as ChatRequestOptions)
apps/pro-web/components/routes/thread/thread-popup.tsx (1)

344-348: Unused parameters should be removed or documented.

The ThreadPopUpCardHeader function has parameters marked as intentionally unused with underscore prefix. If these are kept for API compatibility, consider adding a @deprecated JSDoc comment or removing them if no external callers exist.

/**
 * `@deprecated` messages, isBrowseView, isProView params are no longer used
 * but kept for API compatibility. Remove in next major version.
 */

Comment on lines 86 to 89
const wordMatches = domainSlugs.filter((slug) => {
const regex = new RegExp(`\\b${domainSlug}\\b`)
return regex.test(slug)
})
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

Potential ReDoS vulnerability with user-controlled regex input.

The domainSlug variable is derived from user input and used directly in a RegExp constructor. If domainSlug contains special regex characters or malicious patterns, this could lead to Regular Expression Denial of Service (ReDoS).

🔒 Suggested fix: escape regex special characters
+// Escape special regex characters to prevent ReDoS
+const escapeRegex = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')

 const wordMatches = domainSlugs.filter((slug) => {
-	const regex = new RegExp(`\\b${domainSlug}\\b`)
+	const regex = new RegExp(`\\b${escapeRegex(domainSlug)}\\b`)
 	return regex.test(slug)
 })
📝 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 wordMatches = domainSlugs.filter((slug) => {
const regex = new RegExp(`\\b${domainSlug}\\b`)
return regex.test(slug)
})
// Escape special regex characters to prevent ReDoS
const escapeRegex = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const wordMatches = domainSlugs.filter((slug) => {
const regex = new RegExp(`\\b${escapeRegex(domainSlug)}\\b`)
return regex.test(slug)
})
🧰 Tools
🪛 ast-grep (0.40.5)

[warning] 86-86: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\b${domainSlug}\\b)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🤖 Prompt for AI Agents
In `@apps/pro-web/lib/url.ts` around lines 86 - 89, The filter building
wordMatches uses user-controlled domainSlug directly in new RegExp which can
cause ReDoS; escape regex metacharacters in domainSlug before constructing regex
(or avoid RegExp entirely by using a safe string comparison like slug ===
domainSlug or slug.split/startsWith depending on intent). Update the code around
domainSlugs.filter and the regex creation to call a safe escape helper (e.g.,
escapeRegex(domainSlug)) or replace the regex branch with a non-regex comparison
to safely match word boundaries.

Comment on lines 22 to 27
export const DEBUG_FLAGS = {
workspace:
(typeof window !== 'undefined' &&
Boolean(
(window as typeof window & { __DEBUG_WORKSPACE__?: boolean })
.__DEBUG_WORKSPACE__,
)) ||
true,
content:
(typeof window !== 'undefined' &&
Boolean(
(window as typeof window & { __DEBUG_WORKSPACE_CONTENT__?: boolean })
.__DEBUG_WORKSPACE_CONTENT__,
)) ||
true,
chat:
(typeof window !== 'undefined' &&
Boolean(
(window as typeof window & { __DEBUG_WORKSPACE_CHAT__?: boolean })
.__DEBUG_WORKSPACE_CHAT__,
)) ||
true,
markdown:
(typeof window !== 'undefined' &&
Boolean(
(window as typeof window & { __DEBUG_MARKDOWN__?: boolean })
.__DEBUG_MARKDOWN__,
)) ||
true,
workspace: Boolean(w?.__DEBUG_WORKSPACE__),
content: Boolean(w?.__DEBUG_WORKSPACE_CONTENT__),
chat: Boolean(w?.__DEBUG_WORKSPACE_CHAT__),
markdown: Boolean(w?.__DEBUG_MARKDOWN__),
}
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

Debug flags are evaluated once at module load, not dynamically.

The documented workflow suggests setting flags in devtools at runtime, but DEBUG_FLAGS values are computed when the module loads. Setting window.__DEBUG_WORKSPACE__ = true after page load won't enable debugging—users must reload.

Consider using getters for dynamic evaluation:

🔧 Proposed fix using getters for runtime evaluation
 export const DEBUG_FLAGS = {
-	workspace: Boolean(w?.__DEBUG_WORKSPACE__),
-	content: Boolean(w?.__DEBUG_WORKSPACE_CONTENT__),
-	chat: Boolean(w?.__DEBUG_WORKSPACE_CHAT__),
-	markdown: Boolean(w?.__DEBUG_MARKDOWN__),
+	get workspace() {
+		return typeof window !== 'undefined' && Boolean((window as DebugWindow).__DEBUG_WORKSPACE__)
+	},
+	get content() {
+		return typeof window !== 'undefined' && Boolean((window as DebugWindow).__DEBUG_WORKSPACE_CONTENT__)
+	},
+	get chat() {
+		return typeof window !== 'undefined' && Boolean((window as DebugWindow).__DEBUG_WORKSPACE_CHAT__)
+	},
+	get markdown() {
+		return typeof window !== 'undefined' && Boolean((window as DebugWindow).__DEBUG_MARKDOWN__)
+	},
 }
📝 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
export const DEBUG_FLAGS = {
workspace:
(typeof window !== 'undefined' &&
Boolean(
(window as typeof window & { __DEBUG_WORKSPACE__?: boolean })
.__DEBUG_WORKSPACE__,
)) ||
true,
content:
(typeof window !== 'undefined' &&
Boolean(
(window as typeof window & { __DEBUG_WORKSPACE_CONTENT__?: boolean })
.__DEBUG_WORKSPACE_CONTENT__,
)) ||
true,
chat:
(typeof window !== 'undefined' &&
Boolean(
(window as typeof window & { __DEBUG_WORKSPACE_CHAT__?: boolean })
.__DEBUG_WORKSPACE_CHAT__,
)) ||
true,
markdown:
(typeof window !== 'undefined' &&
Boolean(
(window as typeof window & { __DEBUG_MARKDOWN__?: boolean })
.__DEBUG_MARKDOWN__,
)) ||
true,
workspace: Boolean(w?.__DEBUG_WORKSPACE__),
content: Boolean(w?.__DEBUG_WORKSPACE_CONTENT__),
chat: Boolean(w?.__DEBUG_WORKSPACE_CHAT__),
markdown: Boolean(w?.__DEBUG_MARKDOWN__),
}
export const DEBUG_FLAGS = {
get workspace() {
return typeof window !== 'undefined' && Boolean((window as DebugWindow).__DEBUG_WORKSPACE__)
},
get content() {
return typeof window !== 'undefined' && Boolean((window as DebugWindow).__DEBUG_WORKSPACE_CONTENT__)
},
get chat() {
return typeof window !== 'undefined' && Boolean((window as DebugWindow).__DEBUG_WORKSPACE_CHAT__)
},
get markdown() {
return typeof window !== 'undefined' && Boolean((window as DebugWindow).__DEBUG_MARKDOWN__)
},
}
🤖 Prompt for AI Agents
In `@apps/pro-web/lib/workspace-debug.ts` around lines 22 - 27, DEBUG_FLAGS is
computed once at module load so toggling window.__DEBUG_* at runtime has no
effect; change it to evaluate dynamically by replacing the static boolean values
with runtime getters or functions (e.g., convert DEBUG_FLAGS into an object with
getters for workspace/content/chat/markdown or export a getDebugFlags()
function) so each access reads w?.__DEBUG_WORKSPACE__,
w?.__DEBUG_WORKSPACE_CONTENT__, w?.__DEBUG_WORKSPACE_CHAT__, and
w?.__DEBUG_MARKDOWN__ anew; update any call sites that expect the previous
static object if necessary.

Comment on lines +1 to +377
'use client'

import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
import { ChevronDown, ChevronUp, LayoutTemplate } from 'lucide-react'
import * as React from 'react'

interface PromptEnhancerProps {
setInput: (value: string) => void
input: string
className?: string
selectedModel?: string
}

const ENHANCEMENT_OPTIONS = [
{
id: 'complete-specific',
label: 'Clarify intent',
suffix: ' Please provide a detailed and specific answer.',
},
{
id: 'review-refine',
label: 'Verify and improve',
suffix: ' Please review your answer for accuracy and refine it.',
},
{
id: 'creative-angle',
label: 'Unique insight',
suffix: ' Please answer with a creative perspective.',
},
]

type ViewMode = 'raw' | 'structured'

export function PromptEnhancer({
setInput,
input,
className,
selectedModel = '',
}: PromptEnhancerProps) {
const [open, setOpen] = React.useState(false)
const [activeOptions, setActiveOptions] = React.useState<string[]>([])
const [isExpanded, setIsExpanded] = React.useState(true)
const [viewMode, setViewMode] = React.useState<ViewMode>('raw')

// Structured Data State
const [structuredData, setStructuredData] = React.useState<
Record<string, string>
>({})

// Reset active options when input is cleared
React.useEffect(() => {
if (!input) {
setActiveOptions([])
setStructuredData({})
}
}, [input])

const handleOptionToggle = (optionId: string, suffix: string) => {
const isActive = activeOptions.includes(optionId)
let newOptions: string[]
let newInput = input

if (isActive) {
newOptions = activeOptions.filter((id) => id !== optionId)
newInput = newInput.replace(suffix, '')
} else {
newOptions = [...activeOptions, optionId]
if (!newInput.includes(suffix)) {
newInput = `${newInput}${suffix}`
}
}

setActiveOptions(newOptions)
setInput(newInput)
}

// Model Detection
const isO1 = selectedModel.match(/(o1|gpt-5)/i)
const isClaude = selectedModel.match(/(claude|anthropic)/i)
const isGemini = selectedModel.match(/(gemini)/i)

// Determine Structure Fields
const getStructureFields = () => {
if (isO1) {
return [
{ key: 'goal', label: 'Goal', type: 'input', placeholder: 'I want...' },
{
key: 'returnFormat',
label: 'Return Format',
type: 'input',
placeholder: 'Structure as...',
},
{
key: 'constraints',
label: 'Warnings & Constraints',
type: 'textarea',
placeholder: '- Do NOT...',
},
{
key: 'context',
label: 'Context Dump',
type: 'textarea',
placeholder: 'Background info...',
},
]
}
if (isClaude) {
return [
{
key: 'taskRole',
label: 'Task Context / Role Refinement',
type: 'input',
placeholder: 'Refine global persona (optional)...',
},
{
key: 'context',
label: 'Context',
type: 'textarea',
placeholder: 'Background context...',
},
{
key: 'instruction',
label: 'Instruction',
type: 'textarea',
placeholder: 'Analyze and perform...',
},
{
key: 'outputFormat',
label: 'Output Format',
type: 'textarea',
placeholder: 'Wrap in XML tags...',
},
{
key: 'userQuery',
label: 'User Query',
type: 'textarea',
placeholder: 'The actual request...',
},
]
}
if (isGemini) {
return [
{
key: 'mediaData',
label: 'Media & Data Description',
type: 'textarea',
placeholder: 'Attached file content...',
},
{
key: 'systemInstructions',
label: 'System Instructions',
type: 'textarea',
placeholder: 'Role and task...',
},
{
key: 'examples',
label: 'Few-Shot Examples',
type: 'textarea',
placeholder: 'Input: ... Output: ...',
},
{
key: 'query',
label: 'The Query',
type: 'textarea',
placeholder: 'Based on data above...',
},
]
}
// Default structure
return [
{
key: 'prompt',
label: 'Prompt',
type: 'textarea',
placeholder: 'Enter your prompt...',
},
]
}

const fields = getStructureFields()

const handleStructuredChange = (key: string, value: string) => {
const newData = { ...structuredData, [key]: value }
setStructuredData(newData)
buildPromptFromStructure(newData)
}

const buildPromptFromStructure = (data: Record<string, string>) => {
let assembledProfile = ''

if (isO1) {
if (data.goal)
assembledProfile += `<!-- SECTION 1: GOAL -->\n${data.goal}\n`
if (data.returnFormat)
assembledProfile += `<!-- SECTION 2: RETURN FORMAT -->\n${data.returnFormat}\n`
if (data.constraints)
assembledProfile += `<!-- SECTION 3: WARNINGS & CONSTRAINTS -->\n${data.constraints}\n`
if (data.context)
assembledProfile += `<!-- SECTION 4: CONTEXT DUMP -->\n${data.context}\n`
} else if (isClaude) {
if (data.taskRole)
assembledProfile += `<system_role>${data.taskRole}</system_role>\n`
if (data.context)
assembledProfile += `<context>\n${data.context}\n</context>\n`
if (data.instruction)
assembledProfile += `<instruction>\n${data.instruction}\n</instruction>\n`
if (data.outputFormat)
assembledProfile += `<output_format>\n${data.outputFormat}\n</output_format>\n`
if (data.userQuery)
assembledProfile += `<user_query>\n${data.userQuery}\n</user_query>\n`
} else if (isGemini) {
if (data.mediaData)
assembledProfile += `<!-- PART 1: MEDIA & DATA -->\n${data.mediaData}\n`
if (data.systemInstructions)
assembledProfile += `<!-- PART 2: SYSTEM INSTRUCTIONS -->\n${data.systemInstructions}\n`
if (data.examples)
assembledProfile += `<!-- PART 3: FEW-SHOT EXAMPLES -->\n${data.examples}\n`
if (data.query)
assembledProfile += `<!-- PART 4: THE QUERY -->\n${data.query}\n`
} else {
assembledProfile = data.prompt || ''
}

// Re-append active modifiers
const modifiers = activeOptions
.map((optId) => ENHANCEMENT_OPTIONS.find((o) => o.id === optId)?.suffix)
.filter(Boolean)
.join('')

setInput((assembledProfile.trim() + modifiers).trim())
}

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="sm"
className={cn('text-xs text-muted-foreground', className)}
>
Prompt Enhancer
</Button>
</PopoverTrigger>
<PopoverContent
align="end"
className={cn('p-0', 'w-[80vw] max-w-[800px]')}
>
<div className="flex flex-col">
{/* Header Section with Checkboxes */}
<div className="flex items-center justify-between p-3 border-b bg-muted/30">
<div className="flex items-center gap-3">
<span className="text-sm font-semibold">Enhance:</span>
{ENHANCEMENT_OPTIONS.map((option) => (
<div key={option.id} className="flex items-center space-x-2">
<Checkbox
id={`header-${option.id}`}
checked={activeOptions.includes(option.id)}
onCheckedChange={() =>
handleOptionToggle(option.id, option.suffix)
}
/>
<label
htmlFor={`header-${option.id}`}
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
{option.label}
</label>
</div>
))}
</div>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? (
<ChevronDown className="size-4" />
) : (
<ChevronUp className="size-4" />
)}
</Button>
</div>

{/* Collapsible Content */}
{isExpanded && (
<div className="p-4 space-y-4 bg-background animate-in slide-in-from-top-2 duration-200">
{/* Controls Row */}
<div className="flex items-center justify-between">
<div className="flex items-center bg-muted rounded-md p-1">
<button
type="button"
onClick={() => setViewMode('raw')}
className={cn(
'px-3 py-1 text-sm rounded-sm transition-all',
viewMode === 'raw'
? 'bg-background shadow-sm text-foreground'
: 'text-muted-foreground hover:text-foreground',
)}
>
Raw
</button>
<button
type="button"
onClick={() => setViewMode('structured')}
className={cn(
'px-3 py-1 text-sm rounded-sm transition-all',
viewMode === 'structured'
? 'bg-background shadow-sm text-foreground'
: 'text-muted-foreground hover:text-foreground',
)}
>
Structured
</button>
</div>
<Button variant="outline" size="sm" className="gap-2">
<LayoutTemplate className="size-4" />
Templates
</Button>
</div>

{/* Main Input Area */}
<div className="min-h-[200px] max-h-[60vh] overflow-y-auto pr-1">
{viewMode === 'raw' ? (
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
className="w-full h-[300px] resize-none font-mono text-sm"
placeholder="Enter your prompt here..."
/>
) : (
<div className="space-y-4">
{fields.map((field) => (
<div key={field.key} className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
{field.label}
</Label>
{field.type === 'textarea' ? (
<Textarea
placeholder={field.placeholder}
value={structuredData[field.key] || ''}
onChange={(e) =>
handleStructuredChange(field.key, e.target.value)
}
className="resize-y min-h-[80px]"
/>
) : (
<Input
placeholder={field.placeholder}
value={structuredData[field.key] || ''}
onChange={(e) =>
handleStructuredChange(field.key, e.target.value)
}
/>
)}
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
</PopoverContent>
</Popover>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Significant code duplication with pro-web variant.

This component is nearly identical to apps/pro-web/components/routes/chat/prompt-form/prompt-enhancer.tsx. Consider extracting to a shared package (e.g., packages/shared or packages/ui) to maintain consistency and reduce maintenance burden.

All technical issues noted in the pro-web review apply here as well:

  • Model detection should use .test() for boolean types (lines 88-90)
  • Memoize fields with useMemo (lines 93-190)
  • Use stable function references instead of inline arrows (lines 268-270, 285, 303, 315, 337, 352-353, 361-362)
🤖 Prompt for AI Agents
In `@apps/web/components/routes/chat/prompt-form/prompt-enhancer.tsx` around lines
1 - 377, This duplicates the pro-web PromptEnhancer — extract PromptEnhancer
(and constants like ENHANCEMENT_OPTIONS) into a shared package (e.g.,
packages/shared or packages/ui) and import it here to avoid maintenance drift;
while migrating, fix the technical feedback: change model detection to boolean
checks using RegExp.test() for isO1/isClaude/isGemini instead of .match(),
memoize computed fields by wrapping getStructureFields()/fields in React.useMemo
depending on selectedModel and those boolean flags, and replace inline arrow
callbacks in JSX with stable useCallback handlers (e.g., replace inline
onCheckedChange, onClick for view toggles, onChange for Textarea/Input, Popover
onOpenChange and expand toggle) using named functions like handleOptionToggle,
handleStructuredChange, buildPromptFromStructure to keep stable references and
avoid re-renders.

Comment on lines +492 to +524
index('assistant_answers_thread_doc_scope_idx').using(
'btree',
table.threadId.asc().nullsLast().op('text_ops'),
table.docNodeId.asc().nullsLast().op('text_ops'),
table.scopeType.asc().nullsLast().op('enum_ops'),
table.scopeId.asc().nullsLast().op('text_ops'),
),
index('assistant_answers_thread_doc_scope_turn_idx').using(
'btree',
table.threadId.asc().nullsLast().op('int4_ops'),
table.docNodeId.asc().nullsLast().op('text_ops'),
table.scopeType.asc().nullsLast().op('text_ops'),
table.scopeId.asc().nullsLast().op('int4_ops'),
table.turnIndex.asc().nullsLast().op('enum_ops'),
),
index('assistant_answers_thread_scope_idx').using(
'btree',
table.threadId.asc().nullsLast().op('uuid_ops'),
table.scopeType.asc().nullsLast().op('enum_ops'),
table.scopeId.asc().nullsLast().op('text_ops'),
),
index('assistant_answers_thread_scope_turn_idx').using(
'btree',
table.threadId.asc().nullsLast().op('enum_ops'),
table.scopeType.asc().nullsLast().op('text_ops'),
table.scopeId.asc().nullsLast().op('enum_ops'),
table.turnIndex.desc().nullsFirst().op('uuid_ops'),
),
index('assistant_answers_thread_turn_idx').using(
'btree',
table.threadId.asc().nullsLast().op('uuid_ops'),
table.turnIndex.desc().nullsFirst().op('int4_ops'),
),
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

Incorrect operator classes in index definitions may prevent index usage.

Several indexes have mismatched operator classes for their column types:

Line Column Type Current Op Expected Op
494 threadId uuid text_ops uuid_ops
501 threadId uuid int4_ops uuid_ops
504 scopeId text int4_ops text_ops
505 turnIndex integer enum_ops int4_ops
515 threadId uuid enum_ops uuid_ops
517 scopeId text enum_ops text_ops
518 turnIndex integer uuid_ops int4_ops

PostgreSQL may ignore these indexes during query planning if the operator class doesn't match the column type.

🔧 Proposed fix for operator classes
 		index('assistant_answers_thread_doc_scope_idx').using(
 			'btree',
-			table.threadId.asc().nullsLast().op('text_ops'),
+			table.threadId.asc().nullsLast().op('uuid_ops'),
 			table.docNodeId.asc().nullsLast().op('text_ops'),
 			table.scopeType.asc().nullsLast().op('enum_ops'),
 			table.scopeId.asc().nullsLast().op('text_ops'),
 		),
 		index('assistant_answers_thread_doc_scope_turn_idx').using(
 			'btree',
-			table.threadId.asc().nullsLast().op('int4_ops'),
+			table.threadId.asc().nullsLast().op('uuid_ops'),
 			table.docNodeId.asc().nullsLast().op('text_ops'),
-			table.scopeType.asc().nullsLast().op('text_ops'),
-			table.scopeId.asc().nullsLast().op('int4_ops'),
-			table.turnIndex.asc().nullsLast().op('enum_ops'),
+			table.scopeType.asc().nullsLast().op('enum_ops'),
+			table.scopeId.asc().nullsLast().op('text_ops'),
+			table.turnIndex.asc().nullsLast().op('int4_ops'),
 		),
 		index('assistant_answers_thread_scope_idx').using(
 			'btree',
 			table.threadId.asc().nullsLast().op('uuid_ops'),
 			table.scopeType.asc().nullsLast().op('enum_ops'),
 			table.scopeId.asc().nullsLast().op('text_ops'),
 		),
 		index('assistant_answers_thread_scope_turn_idx').using(
 			'btree',
-			table.threadId.asc().nullsLast().op('enum_ops'),
-			table.scopeType.asc().nullsLast().op('text_ops'),
-			table.scopeId.asc().nullsLast().op('enum_ops'),
-			table.turnIndex.desc().nullsFirst().op('uuid_ops'),
+			table.threadId.asc().nullsLast().op('uuid_ops'),
+			table.scopeType.asc().nullsLast().op('enum_ops'),
+			table.scopeId.asc().nullsLast().op('text_ops'),
+			table.turnIndex.desc().nullsFirst().op('int4_ops'),
 		),
📝 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
index('assistant_answers_thread_doc_scope_idx').using(
'btree',
table.threadId.asc().nullsLast().op('text_ops'),
table.docNodeId.asc().nullsLast().op('text_ops'),
table.scopeType.asc().nullsLast().op('enum_ops'),
table.scopeId.asc().nullsLast().op('text_ops'),
),
index('assistant_answers_thread_doc_scope_turn_idx').using(
'btree',
table.threadId.asc().nullsLast().op('int4_ops'),
table.docNodeId.asc().nullsLast().op('text_ops'),
table.scopeType.asc().nullsLast().op('text_ops'),
table.scopeId.asc().nullsLast().op('int4_ops'),
table.turnIndex.asc().nullsLast().op('enum_ops'),
),
index('assistant_answers_thread_scope_idx').using(
'btree',
table.threadId.asc().nullsLast().op('uuid_ops'),
table.scopeType.asc().nullsLast().op('enum_ops'),
table.scopeId.asc().nullsLast().op('text_ops'),
),
index('assistant_answers_thread_scope_turn_idx').using(
'btree',
table.threadId.asc().nullsLast().op('enum_ops'),
table.scopeType.asc().nullsLast().op('text_ops'),
table.scopeId.asc().nullsLast().op('enum_ops'),
table.turnIndex.desc().nullsFirst().op('uuid_ops'),
),
index('assistant_answers_thread_turn_idx').using(
'btree',
table.threadId.asc().nullsLast().op('uuid_ops'),
table.turnIndex.desc().nullsFirst().op('int4_ops'),
),
index('assistant_answers_thread_doc_scope_idx').using(
'btree',
table.threadId.asc().nullsLast().op('uuid_ops'),
table.docNodeId.asc().nullsLast().op('text_ops'),
table.scopeType.asc().nullsLast().op('enum_ops'),
table.scopeId.asc().nullsLast().op('text_ops'),
),
index('assistant_answers_thread_doc_scope_turn_idx').using(
'btree',
table.threadId.asc().nullsLast().op('uuid_ops'),
table.docNodeId.asc().nullsLast().op('text_ops'),
table.scopeType.asc().nullsLast().op('enum_ops'),
table.scopeId.asc().nullsLast().op('text_ops'),
table.turnIndex.asc().nullsLast().op('int4_ops'),
),
index('assistant_answers_thread_scope_idx').using(
'btree',
table.threadId.asc().nullsLast().op('uuid_ops'),
table.scopeType.asc().nullsLast().op('enum_ops'),
table.scopeId.asc().nullsLast().op('text_ops'),
),
index('assistant_answers_thread_scope_turn_idx').using(
'btree',
table.threadId.asc().nullsLast().op('uuid_ops'),
table.scopeType.asc().nullsLast().op('enum_ops'),
table.scopeId.asc().nullsLast().op('text_ops'),
table.turnIndex.desc().nullsFirst().op('int4_ops'),
),
index('assistant_answers_thread_turn_idx').using(
'btree',
table.threadId.asc().nullsLast().op('uuid_ops'),
table.turnIndex.desc().nullsFirst().op('int4_ops'),
),
🤖 Prompt for AI Agents
In `@packages/mb-drizzle/src/drizzle/schema.ts` around lines 492 - 524, Indexes in
packages/mb-drizzle/src/drizzle/schema.ts use incorrect operator classes causing
Postgres to ignore them; update the .op(...) calls to match each column type:
for index('assistant_answers_thread_doc_scope_idx') change
table.threadId.op('text_ops') -> 'uuid_ops'; for
index('assistant_answers_thread_doc_scope_turn_idx') change
table.threadId.op('int4_ops') -> 'uuid_ops', table.scopeId.op('int4_ops') ->
'text_ops', table.turnIndex.op('enum_ops') -> 'int4_ops'; for
index('assistant_answers_thread_scope_idx') change table.threadId.op('enum_ops')
-> 'uuid_ops'; for index('assistant_answers_thread_scope_turn_idx') change
table.scopeId.op('enum_ops') -> 'text_ops' and table.turnIndex.op('uuid_ops') ->
'int4_ops'; ensure every .op(...) matches the actual column type (uuid ->
uuid_ops, text -> text_ops, integer -> int4_ops, enum -> enum_ops).

Comment on lines +566 to +582
index('document_operations_doc_node_scope_created_idx').using(
'btree',
table.docNodeId.asc().nullsLast().op('timestamptz_ops'),
table.scopeType.asc().nullsLast().op('timestamptz_ops'),
table.scopeId.asc().nullsLast().op('timestamptz_ops'),
table.createdAt.desc().nullsFirst().op('timestamptz_ops'),
),
index('document_operations_doc_node_section_idx').using(
'btree',
table.docNodeId.asc().nullsLast().op('text_ops'),
table.sectionId.asc().nullsLast().op('text_ops'),
),
index('document_operations_document_key_created_idx').using(
'btree',
table.documentKey.asc().nullsLast().op('timestamptz_ops'),
table.createdAt.desc().nullsFirst().op('timestamptz_ops'),
),
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 15, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Mismatched operator classes in documentOperations indexes.

Text columns are using timestamptz_ops instead of text_ops:

Line Column Type Current Op Expected Op
568 docNodeId text timestamptz_ops text_ops
569 scopeType text timestamptz_ops text_ops
570 scopeId text timestamptz_ops text_ops
580 documentKey text timestamptz_ops text_ops
🔧 Proposed fix
 		index('document_operations_doc_node_scope_created_idx').using(
 			'btree',
-			table.docNodeId.asc().nullsLast().op('timestamptz_ops'),
-			table.scopeType.asc().nullsLast().op('timestamptz_ops'),
-			table.scopeId.asc().nullsLast().op('timestamptz_ops'),
+			table.docNodeId.asc().nullsLast().op('text_ops'),
+			table.scopeType.asc().nullsLast().op('text_ops'),
+			table.scopeId.asc().nullsLast().op('text_ops'),
 			table.createdAt.desc().nullsFirst().op('timestamptz_ops'),
 		),
 ...
 		index('document_operations_document_key_created_idx').using(
 			'btree',
-			table.documentKey.asc().nullsLast().op('timestamptz_ops'),
+			table.documentKey.asc().nullsLast().op('text_ops'),
 			table.createdAt.desc().nullsFirst().op('timestamptz_ops'),
 		),
📝 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
index('document_operations_doc_node_scope_created_idx').using(
'btree',
table.docNodeId.asc().nullsLast().op('timestamptz_ops'),
table.scopeType.asc().nullsLast().op('timestamptz_ops'),
table.scopeId.asc().nullsLast().op('timestamptz_ops'),
table.createdAt.desc().nullsFirst().op('timestamptz_ops'),
),
index('document_operations_doc_node_section_idx').using(
'btree',
table.docNodeId.asc().nullsLast().op('text_ops'),
table.sectionId.asc().nullsLast().op('text_ops'),
),
index('document_operations_document_key_created_idx').using(
'btree',
table.documentKey.asc().nullsLast().op('timestamptz_ops'),
table.createdAt.desc().nullsFirst().op('timestamptz_ops'),
),
index('document_operations_doc_node_scope_created_idx').using(
'btree',
table.docNodeId.asc().nullsLast().op('text_ops'),
table.scopeType.asc().nullsLast().op('text_ops'),
table.scopeId.asc().nullsLast().op('text_ops'),
table.createdAt.desc().nullsFirst().op('timestamptz_ops'),
),
index('document_operations_doc_node_section_idx').using(
'btree',
table.docNodeId.asc().nullsLast().op('text_ops'),
table.sectionId.asc().nullsLast().op('text_ops'),
),
index('document_operations_document_key_created_idx').using(
'btree',
table.documentKey.asc().nullsLast().op('text_ops'),
table.createdAt.desc().nullsFirst().op('timestamptz_ops'),
),
🤖 Prompt for AI Agents
In `@packages/mb-drizzle/src/drizzle/schema.ts` around lines 566 - 582, Indexes
document_operations_doc_node_scope_created_idx and
document_operations_document_key_created_idx use the wrong operator class for
text columns; update the op calls for table.docNodeId, table.scopeType,
table.scopeId, and table.documentKey from 'timestamptz_ops' to 'text_ops' (leave
the section index as-is since it already uses text_ops); modify the index
definitions referencing those symbols so the text columns use op('text_ops')
consistently.

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai This isn't database schema issue? This is an auto-generated file that follows a given schema. Right now I am discovering why we need document_operations if we have already thread.metadata.documents which already tracks version change (and active current version) and sections on each document, so as the relationship that it has with an organization project.

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, you raise two important points—let me address both:

1. Auto-generated vs Schema Issue

While Drizzle schemas can be generated from databases, this appears to be a source schema that drives migrations. Regardless of how the file is created, the operator class mismatch is a real PostgreSQL issue:

  • Text columns (docNodeId, scopeType, scopeId, documentKey) should use text_ops
  • Using timestamptz_ops for text columns can cause index build failures or silent performance degradation
  • PostgreSQL won't necessarily error immediately, but the index won't work optimally

2. Why document_operations When thread.metadata.documents Exists?

Let me search the codebase to understand the distinction:


🏁 Script executed:

#!/bin/bash

# Search for thread.metadata.documents usage to understand its structure
echo "=== thread.metadata.documents usage ==="
rg -A5 -B2 "metadata\.documents" --type ts --type tsx

echo -e "\n=== document_operations usage in code ==="
rg "document_operations|documentOperations" --type ts --type tsx -C3

echo -e "\n=== Check migration files for document_operations purpose ==="
fd -e sql . apps/hasura/migrations | xargs grep -l "document_operations" | head -5 | xargs cat

Length of output: 3883


Based on the migration file, I can now explain the architectural distinction:

document_operations vs thread.metadata.documents

They serve complementary but distinct purposes:

thread.metadata.documents (Current State)

  • What it stores: Current document state
  • Purpose: "What IS currently true"
  • Contents: Active version, current sections, organization/project relationship
  • Storage: JSONB (flexible, document-oriented)

document_operations (Audit Trail)

  • What it stores: Operation history
  • Purpose: "What HAPPENED over time"
  • Contents (from migration):
    -- Operation identity
    operation_type text NOT NULL, -- 'rewrite' | 'expand' | 'edit' | 'unknown'
    status text NOT NULL DEFAULT 'started', -- 'started' | 'completed' | 'failed'
    
    -- Prompt payload
    user_prompt text NOT NULL,
    meta_prompt text NULL,
    
    -- Selection info (replay capability)
    selection_start int NULL,
    selection_end int NULL,
    
    -- Version linkage
    version_before int NULL,
    version_after int NULL,
    checksum_before text NULL,
    checksum_after text NULL,
    
    error text NULL

Why Both?

Benefits of separate audit table:

  1. Query operation history without parsing JSONB
  2. Time-series analysis of edit operations (indexed by created_at)
  3. Error tracking/debugging (stores error details per operation)
  4. Replay capability (has prompts + selections + versions)
  5. Performance - doesn't bloat thread.metadata, efficient time-based indexes
  6. Best-effort logging (see AI summary: "best-effort logging without blocking UX")

The migration comment confirms this design: "store document-edit prompts separately... Keep additive + safe patterns; do not modify existing chat tables."


Regarding the operator class issue: Even if auto-generated, the schema should be corrected to prevent PostgreSQL index issues. Would you like me to verify if this matches the actual migration SQL, or if there's a generation mismatch?


🏁 Script executed:

#!/bin/bash

# Search for thread.metadata usage patterns
echo "=== thread.metadata usage ==="
rg "thread\.metadata" -t ts -C2 | head -80

echo -e "\n=== Log document operation usage ==="
rg "logDocumentOperation" -t ts -C5 | head -80

echo -e "\n=== Check AI summary for document_operations purpose ==="
cat << 'EOF'
From the AI summary, the document_operations table purpose is described in:
apps/hasura/migrations/masterbots/1767498310896_add_public_document_operations/up.sql
EOF

Length of output: 11309


@AndlerRL, excellent question! I found something important: there's a generation mismatch.

🐛 Operator Class Issue is REAL (Generation Bug)

Comparing the migration SQL (source of truth) vs Drizzle schema:

Migration SQL (✅ Correct):

CREATE INDEX IF NOT EXISTS document_operations_doc_node_scope_created_idx
ON public.document_operations (doc_node_id, scope_type, scope_id, created_at DESC);
-- ^ PostgreSQL infers text_ops for text columns automatically

Drizzle Schema (❌ Incorrect):

table.docNodeId.asc().nullsLast().op('timestamptz_ops'),  // WRONG!
table.scopeType.asc().nullsLast().op('timestamptz_ops'),  // WRONG!

This suggests Drizzle's schema introspection generated incorrect operator classes. The migration is correct, but the TypeScript schema needs manual correction.


📊 Architecture: Why Both Tables?

From the codebase, here's the concrete evidence:

thread.metadata (Current State - JSONB)

// apps/pro-web/lib/cron/refresh-workspace-document-links.ts
sql`${thread.metadata}::jsonb ? 'documents' AND 
    jsonb_array_length(${thread.metadata}::jsonb->'documents') > 0`

// apps/pro-web/components/routes/thread/thread-list.tsx  
const docs = (thread.metadata as { documents?: DocMeta[] } | null)?.documents

Purpose: Stores current document list, attachments, relationships

document_operations (Audit Trail - Relational)

// apps/pro-web/lib/hooks/use-workspace-chat.tsx
docOpIdPromiseRef.current = logDocumentOperation({
  mode: 'start',
  threadId, project, documentName, documentKey,
  docNodeId, sectionId, scopeType, scopeId,
  operationType,
  userPrompt, metaPrompt,
  selectionStart, selectionEnd,
  versionBefore, checksumBefore,
})

// Later...
await logDocumentOperation({
  mode: 'complete',
  id: opId,
  versionAfter, checksumAfter,
})

// Or on error...
await logDocumentOperation({
  mode: 'fail',
  id: opId,
  error: error.message,
})

Purpose: Operation audit trail with lifecycle tracking

Key Differences

Aspect thread.metadata.documents document_operations
Type JSONB (flexible) Relational (queryable)
Content Current state Historical operations
Query Parse JSONB Indexed columns
Use Case "What docs exist now?" "What operations happened?"
Performance Full scan for history Time-indexed queries
Debugging Current values only Full audit trail with prompts, errors

Real-World Benefit

// Best-effort logging - doesn't block UX
// apps/pro-web/app/actions/document-operations/log-document-operation.ts
export async function logDocumentOperation(args) {
  // NOTE: This action must be best-effort for zero regressions.
  // Callers should catch errors / never block UX.
  try {
    // ... insert/update document_operations
  } catch (err) {
    console.error('Failed to log operation:', err)
    // Doesn't throw - operation continues
  }
}

This allows debugging edit failures (what prompt? what selection? what error?) without affecting user experience.


🔧 Recommended Fix

  1. Operator classes: Update Drizzle schema manually or regenerate with correct introspection
  2. Keep both tables: They serve different purposes (state vs audit), and the code is already using document_operations for observability

Would you like me to generate a corrected version of the index definitions in the schema?


🧠 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.

Comment on lines +906 to +946
index('message_doc_node_id_idx').using(
'btree',
table.docNodeId.asc().nullsLast().op('text_ops'),
),
index('message_thread_doc_created_idx').using(
'btree',
table.threadId.asc().nullsLast().op('uuid_ops'),
table.docNodeId.asc().nullsLast().op('uuid_ops'),
table.createdAt.desc().nullsFirst().op('text_ops'),
),
index('message_thread_doc_scope_created_idx').using(
'btree',
table.threadId.asc().nullsLast().op('enum_ops'),
table.docNodeId.asc().nullsLast().op('timestamptz_ops'),
table.scopeType.asc().nullsLast().op('timestamptz_ops'),
table.scopeId.asc().nullsLast().op('text_ops'),
table.createdAt.desc().nullsFirst().op('enum_ops'),
),
index('message_thread_scope_created_at_idx').using(
'btree',
table.threadId.asc().nullsLast().op('text_ops'),
table.scopeType.asc().nullsLast().op('enum_ops'),
table.scopeId.asc().nullsLast().op('text_ops'),
table.createdAt.asc().nullsLast().op('uuid_ops'),
),
index('message_thread_scope_qchain_idx')
.using(
'btree',
table.threadId.asc().nullsLast().op('uuid_ops'),
table.scopeType.asc().nullsLast().op('text_ops'),
table.scopeId.asc().nullsLast().op('text_ops'),
table.createdAt.asc().nullsLast().op('uuid_ops'),
)
.where(sql`((role = 'user'::text) AND (exclude_from_q_chain = false))`),
index('message_thread_scope_recent_idx').using(
'btree',
table.threadId.asc().nullsLast().op('uuid_ops'),
table.scopeType.asc().nullsLast().op('text_ops'),
table.scopeId.asc().nullsLast().op('enum_ops'),
table.createdAt.desc().nullsFirst().op('text_ops'),
),
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

Multiple operator class mismatches in message table indexes.

The new indexes have several mismatched operator classes:

Line Column Type Current Op Expected Op
913 docNodeId text uuid_ops text_ops
914 createdAt timestamp text_ops timestamptz_ops
918 threadId uuid enum_ops uuid_ops
919 docNodeId text timestamptz_ops text_ops
920 scopeType enum timestamptz_ops enum_ops
922 createdAt timestamp enum_ops timestamptz_ops
926 threadId uuid text_ops uuid_ops
929 createdAt timestamp uuid_ops timestamptz_ops
937 createdAt timestamp uuid_ops timestamptz_ops
944 scopeId text enum_ops text_ops
945 createdAt timestamp text_ops timestamptz_ops
🔧 Proposed fix
 		index('message_thread_doc_created_idx').using(
 			'btree',
 			table.threadId.asc().nullsLast().op('uuid_ops'),
-			table.docNodeId.asc().nullsLast().op('uuid_ops'),
-			table.createdAt.desc().nullsFirst().op('text_ops'),
+			table.docNodeId.asc().nullsLast().op('text_ops'),
+			table.createdAt.desc().nullsFirst().op('timestamptz_ops'),
 		),
 		index('message_thread_doc_scope_created_idx').using(
 			'btree',
-			table.threadId.asc().nullsLast().op('enum_ops'),
-			table.docNodeId.asc().nullsLast().op('timestamptz_ops'),
-			table.scopeType.asc().nullsLast().op('timestamptz_ops'),
+			table.threadId.asc().nullsLast().op('uuid_ops'),
+			table.docNodeId.asc().nullsLast().op('text_ops'),
+			table.scopeType.asc().nullsLast().op('enum_ops'),
 			table.scopeId.asc().nullsLast().op('text_ops'),
-			table.createdAt.desc().nullsFirst().op('enum_ops'),
+			table.createdAt.desc().nullsFirst().op('timestamptz_ops'),
 		),
 		index('message_thread_scope_created_at_idx').using(
 			'btree',
-			table.threadId.asc().nullsLast().op('text_ops'),
+			table.threadId.asc().nullsLast().op('uuid_ops'),
 			table.scopeType.asc().nullsLast().op('enum_ops'),
 			table.scopeId.asc().nullsLast().op('text_ops'),
-			table.createdAt.asc().nullsLast().op('uuid_ops'),
+			table.createdAt.asc().nullsLast().op('timestamptz_ops'),
 		),
 		index('message_thread_scope_qchain_idx')
 			.using(
 				'btree',
 				table.threadId.asc().nullsLast().op('uuid_ops'),
-				table.scopeType.asc().nullsLast().op('text_ops'),
+				table.scopeType.asc().nullsLast().op('enum_ops'),
 				table.scopeId.asc().nullsLast().op('text_ops'),
-				table.createdAt.asc().nullsLast().op('uuid_ops'),
+				table.createdAt.asc().nullsLast().op('timestamptz_ops'),
 			)
 			.where(sql`((role = 'user'::text) AND (exclude_from_q_chain = false))`),
 		index('message_thread_scope_recent_idx').using(
 			'btree',
 			table.threadId.asc().nullsLast().op('uuid_ops'),
-			table.scopeType.asc().nullsLast().op('text_ops'),
-			table.scopeId.asc().nullsLast().op('enum_ops'),
-			table.createdAt.desc().nullsFirst().op('text_ops'),
+			table.scopeType.asc().nullsLast().op('enum_ops'),
+			table.scopeId.asc().nullsLast().op('text_ops'),
+			table.createdAt.desc().nullsFirst().op('timestamptz_ops'),
 		),
🤖 Prompt for AI Agents
In `@packages/mb-drizzle/src/drizzle/schema.ts` around lines 906 - 946, The new
message table index definitions (index names message_doc_node_id_idx,
message_thread_doc_created_idx, message_thread_doc_scope_created_idx,
message_thread_scope_created_at_idx, message_thread_scope_qchain_idx,
message_thread_scope_recent_idx) use incorrect operator classes for several
columns; update the .op(...) calls on the referenced columns (table.docNodeId,
table.createdAt, table.threadId, table.scopeType, table.scopeId) to match their
actual column types: use text_ops for text columns (docNodeId, scopeId),
uuid_ops for uuid columns (threadId), timestamptz_ops for timestamp/timestamptz
columns (createdAt), and enum_ops for enum columns (scopeType); ensure each
index's .op(...) entries are corrected accordingly in the index(...) .using(...)
definitions where those columns appear.

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: 2

🤖 Fix all issues with AI agents
In `@apps/pro-web/app/actions/chat-memory/embedding.ts`:
- Around line 10-41: embedText1536 can hang because the fetch to OpenAI has no
timeout; wrap the fetch in an AbortController with a configurable timeout (e.g.,
10s), pass controller.signal into the fetch call, start a setTimeout that calls
controller.abort() after the timeout, and clear the timer after a successful
response (and on error) so the timer doesn't leak; ensure you catch an abort
error and rethrow a meaningful Error (e.g., "OpenAI embeddings request timed
out") before continuing to parse the response.

In `@apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts`:
- Around line 147-318: The query currently hardcodes minCosine's GraphQL var
type to "float8" in makeSearchQuery and never adapts on GraphQL type errors;
update makeSearchQuery and searchAssistantAnswers to treat minCosine like
queryEmbedding: add a minCosineVarType parameter (default "float8") to
makeSearchQuery and use it in varDefs (`$minCosine: ${minCosineVarType}!`),
ensure the hasuraGql call still passes minCosine value, and inside the retry
loop call tryExtractExpectedType(msg, 'minCosine') (e.g., expMinCosine) and if
present assign minCosineVarType = expMinCosine and continue so the code retries
with the corrected numeric type; reference functions/identifiers:
makeSearchQuery, searchAssistantAnswers, minCosine, tryExtractExpectedType.
♻️ Duplicate comments (4)
apps/pro-web/components/routes/chat/chat-list/index.tsx (1)

109-119: Type casting improved but runtime validation still absent.

The casts have been simplified from double assertions, which is an improvement. However, the underlying concern remains: if upstream message shapes diverge from ThreadMessage, this will fail silently at runtime.

Consider adding a lightweight runtime check or normalizer as previously suggested, especially since messages is typed as unknown[].

♻️ Optional: minimal runtime guard
function isThreadMessage(m: unknown): m is ThreadMessage {
  return (
    typeof m === 'object' &&
    m !== null &&
    'content' in m &&
    'role' in m
  )
}

const chatMessages = (messages || activeThread?.messages || [])
  .filter(isThreadMessage)
apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx (2)

92-97: Incomplete timer cleanup on unmount.

The cleanup effect clears typingTimeoutRef and sectionSaveTimerRef, but sourceSaveTimerRef (declared at line 282) is not cleaned up. This could cause a state update on an unmounted component if a source save is pending during unmount.

🔧 Suggested fix
 useEffect(() => {
 	return () => {
 		if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current)
 		if (sectionSaveTimerRef.current) clearTimeout(sectionSaveTimerRef.current)
+		if (sourceSaveTimerRef.current) clearTimeout(sourceSaveTimerRef.current)
 	}
 }, [])

Note: This requires moving sourceSaveTimerRef declaration (line 282) before this effect, or restructuring the refs together near line 87.


304-336: Sections/Source buttons don't toggle viewMode.

Both buttons display styling based on viewMode but clicking them only calls setIsPreview(false). The component doesn't receive a handler to change viewMode, so users cannot switch between sections and source views using these buttons.

Either pass handleViewSourceToggle as a prop and wire it to these buttons, or remove the buttons if view mode switching is handled elsewhere.

🔧 Suggested fix

Add a prop and wire the handlers:

 export type WorkspaceTextEditorProps = {
 	// ... existing props
+	handleViewSourceToggle: (mode: 'sections' | 'source') => void
 }

Then update the button handlers:

 <Button
 	type="button"
 	variant={viewMode === 'sections' ? 'secondary' : 'outline'}
 	size="sm"
-	onClick={() => setIsPreview(false)}
+	onClick={() => {
+		handleViewSourceToggle('sections')
+		setIsPreview(false)
+	}}
 	disabled={isBusy}
 >
 	<FileText className="size-4 mr-2" />
 	Sections
 </Button>

 <Button
 	type="button"
 	variant={viewMode === 'source' ? 'secondary' : 'outline'}
 	size="sm"
-	onClick={() => setIsPreview(false)}
+	onClick={() => {
+		handleViewSourceToggle('source')
+		setIsPreview(false)
+	}}
 	disabled={isBusy}
 >
 	<TextCursorInputIcon className="size-4 mr-2" />
 	Source
 </Button>
apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts (1)

152-171: Avoid silently swallowing non-schema insert failures.

Non-schema errors are dropped with no trace, which makes ingestion issues hard to diagnose. Logging (or otherwise surfacing) the failure would help without breaking chat flow.

🔧 Suggested fix
 	} catch (error) {
 		// Retry once without doc_node_id if that was the cause (schema lag during rollout).
 		if (docNodeId && typeof (error as Error)?.message === 'string') {
 			const msg = (error as Error).message
 			if (
 				msg.includes('doc_node_id') ||
 				msg.includes('field') ||
 				msg.includes('assistant_answers_insert_input')
 			) {
 				try {
 					const { doc_node_id: _ignored, ...withoutDocNode } = baseObject
 					await hasuraGql(insert, { object: withoutDocNode })
+					return
 				} catch {
+					console.warn('[ingestAssistantAnswer] Retry without doc_node_id failed')
 					return
 				}
 			}
 		}
+		console.warn('[ingestAssistantAnswer] Insert failed:', (error as Error)?.message)
 		return
 	}
🧹 Nitpick comments (7)
apps/pro-web/components/routes/chat/chat-list/index.tsx (2)

186-199: Redundant ?? undefined after type assertion.

On lines 188-189, the ?? undefined is unnecessary since the type assertion already includes | undefined:

-const md =
-  (activeThread?.metadata as Record<string, unknown> | undefined) ??
-  undefined
+const md = activeThread?.metadata as Record<string, unknown> | undefined

The same pattern repeats at lines 217 and 218-219.


213-234: Comprehensive but verbose doc-anchor detection.

The hasDocAnchor function defensively checks multiple possible field paths (doc_node_id, docNodeId, in metadata, in mb_op). While thorough, this suggests inconsistent data shapes upstream. Consider documenting which path is canonical and standardizing upstream if possible.

apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx (2)

381-441: Consider extracting repeated selection range update logic.

The onBlur, onKeyUp, and onChange handlers (lines 391-426) all compute and set the global selection range with similar logic. This is a minor duplication that could be consolidated into a helper function.

♻️ Optional refactor
const updateSelectionFromTextarea = useCallback(
	(el: HTMLTextAreaElement) => {
		if (!activeSectionObj) return
		setGlobalSelectionRange({
			start: activeSectionObj.contentStart + el.selectionStart,
			end: activeSectionObj.contentStart + el.selectionEnd,
		})
	},
	[activeSectionObj, setGlobalSelectionRange],
)

Then use it in handlers:

 onBlur={(e) => {
 	setIsFocused(false)
-	if (!activeSectionObj) return
-	const startRel = e.currentTarget.selectionStart
-	const endRel = e.currentTarget.selectionEnd
-	setGlobalSelectionRange({
-		start: activeSectionObj.contentStart + startRel,
-		end: activeSectionObj.contentStart + endRel,
-	})
+	updateSelectionFromTextarea(e.currentTarget)
 }}

510-526: Consider using interface for props type definition.

Per coding guidelines, interface is preferred for object shapes while type is for unions/tuples/aliases. This props definition represents an object shape.

♻️ Suggested refactor
-export type WorkspaceTextEditorProps = {
+export interface WorkspaceTextEditorProps {
 	activeSection: string | null
 	setActiveSection: (sectionId: string | null) => void
 	viewMode: 'sections' | 'source'

 	sectionTextareaRef: React.RefObject<HTMLTextAreaElement>
 	sourceTextareaRef: React.RefObject<HTMLTextAreaElement>

 	handleCursorPositionChange: (e?: React.SyntheticEvent) => void

 	handleSectionClick: (sectionId: string) => void
 	markUserTyping: () => void

 	handleExpandSection: (sectionTitle: string) => void
 	handleRewriteSection: (sectionTitle: string) => void
 }
CLAUDE.md (1)

116-231: Excellent architecture documentation restructuring.

The reorganization significantly improves the documentation:

  • Clear separation of Core Applications vs Shared Packages (lines 122-135)
  • New "State Management Patterns" section documents the provider architecture and data flow patterns (lines 154-174)
  • "AI Integration Environment" section provides context for the RAG infrastructure being added (lines 176-191)
  • Expanded "GraphQL Integration" guidance aligns with new database operations (lines 192-207)
  • Comprehensive "Development Guidelines" with external documentation links (lines 208-231)

These changes align well with the established patterns documented in learnings and support the PR's RAG feature additions.

💡 Optional: Add code examples for RORO pattern and event handlers

Line 275 - RORO Pattern Example:

Consider adding a code example similar to the named exports example:

// RORO pattern example
// Prefer this
export function createUser({ name, email, role }: CreateUserParams): CreateUserResult {
  // ...
  return { user, error: null };
}

// Over this
export function createUser(name: string, email: string, role: string) {
  // ...
  return user;
}

Line 277 - Event Handler Example:

The guidance "use regular function calls instead of arrow functions" could be clarified:

// Prefer this - stable function reference
function handleClick() {
  // ...
}

<Button onClick={handleClick} />

// Avoid this - creates new function on each render
<Button onClick={() => handleClick()} />
apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts (1)

175-211: Consider routing Hasura calls through hasura.service.ts.

Centralizing GraphQL concerns (auth, error shaping, retries) reduces duplication and keeps Hasura integration consistent across the app. If server actions can’t use it, please confirm the constraint. As per coding guidelines, ...

apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts (1)

515-547: Prefer the shared Hasura service layer.

Using hasura.service.ts here would centralize auth/error handling and reduce duplicate GraphQL plumbing. As per coding guidelines, ...

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b71ddeb and d1b51b2.

📒 Files selected for processing (9)
  • AGENTS.md
  • CLAUDE.md
  • README.md
  • apps/hasura/migrations/masterbots/1766624501677_assistant_answers/down.sql
  • apps/pro-web/app/actions/chat-memory/embedding.ts
  • apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts
  • apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts
  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/hasura/migrations/masterbots/1766624501677_assistant_answers/down.sql
🧰 Additional context used
📓 Path-based instructions (8)
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Use lowercase with dash-separated directory and file names; use specific extensions: .config.ts, .test.ts, .context.tsx, .type.ts, .hook.ts; components may omit extension
Use Bun for package management and scripts instead of npm/yarn
Use Biome for formatting and linting via bun format-and-lint:fix; avoid Prettier/ESLint configs

Files:

  • README.md
  • apps/pro-web/app/actions/chat-memory/embedding.ts
  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • CLAUDE.md
  • apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts
  • apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts
  • AGENTS.md
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
apps/{web,pro-web}/app/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

apps/{web,pro-web}/app/**: Prefer Next.js Server Components; use 'use client' only when necessary
Leverage Streaming and Suspense for responsiveness in Next.js 15
Follow Next.js file conventions in the App Router

apps/{web,pro-web}/app/**: Leverage Server Components by default; use the use client directive only when client-side features are needed
Implement Nested Layouts using the App Router
Use Streaming and Suspense features in Next.js 14/15 where appropriate
Follow Next.js file conventions for routing and layout in the App Router
Optimize data fetching: fetch on the server, fetch where needed, and use preload patterns to avoid waterfalls

Files:

  • apps/pro-web/app/actions/chat-memory/embedding.ts
  • apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts
  • apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Avoid default exports; prefer named exports
Use meaningful variable names; prefix booleans with is/has/does/should
Use interface for objects/classes; type for unions/tuples/aliases; enum for fixed sets; const for literals
Avoid any; prefer specific types
Leverage type inference where clear; annotate function params and return types
Prefer type assertions with 'as' over angle-bracket syntax
Use type guards to narrow types in conditionals

**/*.{ts,tsx}: Avoid default exports; prefer named exports
Use modern JavaScript features with TypeScript across the codebase
Use meaningful names; prefix booleans with is/has/does/should (e.g., isDisabled)
Receive an object and return an object (RORO) for functions interfacing with external services
Type definitions: use interface for objects/classes; type for unions/tuples/aliases; const for literals; enum for fixed sets
Avoid any; prefer specific types
Leverage type inference; omit explicit annotations when obvious
Add explicit type annotations for function parameters and return values
Prefer type assertions with as over angle-bracket syntax
Use type guards to narrow types within conditionals
Favor composition over inheritance in code structure

**/*.{ts,tsx}: Avoid default exports; prefer named exports
Prefer RORO (Receive an object, return an object) for function signatures, especially for external service interactions
Use interface for object shapes/classes; type for unions/tuples/aliases; const for literals; enum for fixed enumerations
Avoid using any; prefer precise types
Leverage type inference when clear; omit redundant annotations
Explicitly annotate function parameters and return types
Prefer type assertions with as over angle-bracket syntax
Use type guards to narrow types safely
Use meaningful variable names; prefix booleans with is/has/does/should
Favor composition over inheritance
Use lowercase dash-separated names for files and folders; use extensions like .config.ts, .test.ts, .context.tsx, .typ...

Files:

  • apps/pro-web/app/actions/chat-memory/embedding.ts
  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts
  • apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
apps/{web,pro-web}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

apps/{web,pro-web}/**/*.{ts,tsx}: Use lowercase kebab-case for directories and filenames (components can omit extension)
Verify DOM operations and consider timing (RAF/delays) when manipulating the DOM in React
Use Tailwind CSS utility classes for styling React components

apps/{web,pro-web}/**/*.{ts,tsx}: Use TypeScript to define React props types (interfaces for props)
Use custom hooks (useMBChat, useThread, useSidebar, useModel) for domain-specific state and keep providers focused and composed hierarchically
Separate presentation from business logic and compose small components
Use hasura.service.ts for all GraphQL operations from UI layers

apps/{web,pro-web}/**/*.{ts,tsx}: Prefer Server Components; use the use client directive only when client-side features are required
Implement Nested Layouts using the App Router
Use Streaming and Suspense in Next.js 14/15 where appropriate
Follow Next.js file conventions as per documentation
Use IndexedDB for local caching to enable immediate UI updates

Files:

  • apps/pro-web/app/actions/chat-memory/embedding.ts
  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts
  • apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
apps/{web,pro-web}/components/routes/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Place page-specific components under components/routes/[feature]/

Organize page-specific components under components/routes/

Files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
apps/{web,pro-web}/**/*.tsx

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

apps/{web,pro-web}/**/*.tsx: Declare React components with the function keyword
Name React components using PascalCase
Order component files: imports → constants → component → styled components → types/interfaces
Use functional components and hooks; type props with a TypeScript interface
Use regular function references (not inline arrows) for React event handlers to avoid extra re-renders

apps/{web,pro-web}/**/*.tsx: Use regular function references for event handlers instead of inline arrow functions in JSX
Declare React components with the function keyword (function ComponentName() {})
Use PascalCase for React component names
Order React component files: imports, constants, component, styled components, types/interfaces
Style components with Tailwind CSS utility classes

Files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

**/*.tsx: Avoid inline arrow functions in JSX props; use stable function references for event handlers
Declare React components with the function keyword (functional components)
Use PascalCase for React component names
Use functional components and hooks instead of class components
Type component props with TypeScript interfaces

Files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
apps/{web,pro-web}/components/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Order component files: imports → constants → component → styled components → types/interfaces

Files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
🧠 Learnings (77)
📓 Common learnings
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.
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.
📚 Learning: 2024-12-05T01:08:50.085Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2024-12-05T01:08:50.085Z
Learning: The project is a monorepo using Turborepo with Next.js frontend (apps/masterbots.ai), Hasura backend (apps/hasura), and shared packages for types, GraphQL operations, and utilities.

Applied to files:

  • README.md
  • CLAUDE.md
  • AGENTS.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Run backend with task boot before frontend development

Applied to files:

  • README.md
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to **/* : Use Bun for package management and scripts instead of npm/yarn

Applied to files:

  • README.md
  • AGENTS.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Use Bun for all package management operations instead of npm/yarn

Applied to files:

  • README.md
📚 Learning: 2025-06-13T08:55:45.217Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 508
File: apps/masterbots.ai/lib/helpers/ai-helpers.ts:280-283
Timestamp: 2025-06-13T08:55:45.217Z
Learning: In `apps/masterbots.ai/lib/helpers/ai-helpers.ts` the `verifyDuplicateMessage` function should:
• Use `message.content` as the sole de-duplication key so provisional (no-slug) and persisted (slugged) messages merge properly.
• Return the full `message` object (not just a string key) for the continuation prompts (`CONTINUE_GENERATION_PROMPT`, `CONTINUE_GENERATION_PROMPT_2`) because their content repeats and they must remain distinct.
• No explicit `!message.content` guard is needed; `return message.content` already yields `undefined` when content is missing.

Applied to files:

  • apps/pro-web/app/actions/chat-memory/embedding.ts
  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts
  • apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts
📚 Learning: 2025-02-17T16:51:19.102Z
Learnt from: Bran18
Repo: bitcashorg/masterbots PR: 376
File: apps/masterbots.ai/components/routes/chat/chat.tsx:92-102
Timestamp: 2025-02-17T16:51:19.102Z
Learning: For the Chat component in apps/masterbots.ai/components/routes/chat/chat.tsx, the developer prefers to verify potential optimizations through local testing before accepting suggestions, particularly for performance-related changes like useCallback usage in debounce implementations.

Applied to files:

  • apps/pro-web/app/actions/chat-memory/embedding.ts
  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-07-02T19:46:45.701Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 519
File: apps/masterbots.ai/lib/hooks/use-chat-attachments.ts:276-297
Timestamp: 2025-07-02T19:46:45.701Z
Learning: In `apps/masterbots.ai/lib/hooks/use-chat-attachments.ts`, the `validateTextContent` function intentionally uses `slugify()` with character removal (`remove: /[^\w\s]/g`) followed by splitting on hyphens to count words. This approach is preferred over simple whitespace splitting because it removes special characters (like those found in code text) and counts only "true words", making it more robust for validating pasted content that may contain mixed text and code.

Applied to files:

  • apps/pro-web/app/actions/chat-memory/embedding.ts
📚 Learning: 2024-11-14T21:12:19.789Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 302
File: apps/masterbots.ai/lib/hooks/use-mb-chat.ts:0-0
Timestamp: 2024-11-14T21:12:19.789Z
Learning: In `apps/masterbots.ai/lib/hooks/use-mb-chat.ts`, for handling message ordering issues, prefer simple solutions like using `setTimeout` over adding sequence numbers to the message schema, to keep the codebase simple.

Applied to files:

  • apps/pro-web/app/actions/chat-memory/embedding.ts
  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • AGENTS.md
📚 Learning: 2025-04-26T10:58:44.341Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 465
File: apps/masterbots.ai/components/routes/browse/browse-list.tsx:1-1
Timestamp: 2025-04-26T10:58:44.341Z
Learning: The browse-list.tsx component is redundant as thread-list.tsx has been developed to support all the same functionality including public pages. The codebase should be cleaned up to remove browse-list.tsx and related components in the browse directory, along with the BrowseProvider context.

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to apps/{web,pro-web}/**/*.{ts,tsx} : Use custom hooks (useMBChat, useThread, useSidebar, useModel) for domain-specific state and keep providers focused and composed hierarchically

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • CLAUDE.md
  • AGENTS.md
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-04-26T10:58:44.341Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 465
File: apps/masterbots.ai/components/routes/browse/browse-list.tsx:1-1
Timestamp: 2025-04-26T10:58:44.341Z
Learning: The browse-list.tsx component and related components in the browse directory are redundant since thread-list.tsx has been developed to support all pages including public pages, and should be removed to simplify the codebase.

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
📚 Learning: 2025-10-24T20:40:56.510Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 573
File: apps/pro-web/components/routes/pro/pro.tsx:49-49
Timestamp: 2025-10-24T20:40:56.510Z
Learning: In apps/pro-web/lib/hooks/use-mb-chat.tsx, the useMBChat() hook returns a tuple [MBChatHookState, MBChatHookActions], not a direct object. Use array destructuring to access state (index 0) and actions (index 1), e.g., const [state, actions] = useMBChat() or const [{ isLoading }] = useMBChat().

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to **/*.{ts,tsx} : Prefer type assertions with as over angle-bracket syntax

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to **/*.{ts,tsx} : Prefer type assertions with 'as' over angle-bracket syntax

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to apps/{web,pro-web}/**/*.{ts,tsx} : Prefer Server Components; use the use client directive only when client-side features are required

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to apps/{web,pro-web}/**/*.tsx : Use functional components and hooks; type props with a TypeScript interface

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to apps/{web,pro-web}/**/*.{ts,tsx} : Separate presentation from business logic and compose small components

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • CLAUDE.md
  • AGENTS.md
📚 Learning: 2024-12-11T19:40:37.103Z
Learnt from: Bran18
Repo: bitcashorg/masterbots PR: 324
File: apps/masterbots.ai/components/routes/browse/browse-chatbot-mobile-details.tsx:145-146
Timestamp: 2024-12-11T19:40:37.103Z
Learning: In `apps/masterbots.ai/components/routes/browse/browse-chatbot-mobile-details.tsx`, the follower and following counts are currently hardcoded as these features are being developed in other PRs.

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
📚 Learning: 2025-04-01T03:48:44.950Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 428
File: apps/masterbots.ai/lib/hooks/use-mb-chat.tsx:196-216
Timestamp: 2025-04-01T03:48:44.950Z
Learning: The `getCurrentSearchParams` function in `use-mb-chat.tsx` is intentionally using `window.location.search` to handle cases where the `useSearchParams` hook doesn't update when the page is reloaded. This is safe because the component has the 'use client' directive.

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
📚 Learning: 2024-12-05T01:14:50.365Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2024-12-05T01:14:50.365Z
Learning: The masterbots.ai project follows Next.js 14 App Router conventions with server components as default and client components marked explicitly with 'use client' directive when needed.

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • CLAUDE.md
  • AGENTS.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to apps/{web,pro-web}/**/*.{ts,tsx} : Implement Nested Layouts using the App Router

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
  • AGENTS.md
📚 Learning: 2024-11-20T04:00:45.413Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 300
File: apps/masterbots.ai/components/routes/thread/user-thread-panel.tsx:120-123
Timestamp: 2024-11-20T04:00:45.413Z
Learning: In React code, when reviewing `useEffect` dependencies, it's not always necessary to add functions like `handleThreadsChange` to the dependency array, as this may lead to unnecessary re-renders and performance issues. Only include dependencies that are essential and may change.

Applied to files:

  • apps/pro-web/components/routes/chat/chat-list/index.tsx
📚 Learning: 2025-08-26T06:40:51.075Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2025-08-26T06:40:51.075Z
Learning: The masterbots project uses a dual-app architecture with apps/web (masterbots.ai) and apps/pro-web (pro.masterbots.ai), where code duplication exists between the apps that should be addressed with a shared packages/ui package.

Applied to files:

  • CLAUDE.md
  • AGENTS.md
📚 Learning: 2024-12-05T01:08:50.085Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2024-12-05T01:08:50.085Z
Learning: The codebase follows TypeScript with strict type checking and functional programming principles. Code organization follows a feature-based structure with dedicated directories for types, utils, and API endpoints.

Applied to files:

  • CLAUDE.md
  • AGENTS.md
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to apps/hasura/migrations/masterbots/** : Store and manage Hasura migrations under apps/hasura/migrations/masterbots/

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to apps/{web,pro-web}/**/*.{ts,tsx} : Follow Next.js file conventions as per documentation

Applied to files:

  • CLAUDE.md
  • AGENTS.md
📚 Learning: 2024-12-05T01:08:50.085Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2024-12-05T01:08:50.085Z
Learning: The frontend uses shadcn/ui components and follows a strict component organization with proper separation of client/server components in Next.js.

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-11-21T19:47:50.195Z
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.

Applied to files:

  • CLAUDE.md
  • AGENTS.md
📚 Learning: 2024-11-06T05:58:49.546Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 295
File: apps/masterbots.ai/app/b/[id]/layout.tsx:13-18
Timestamp: 2024-11-06T05:58:49.546Z
Learning: In the codebase, the main layout (`apps/masterbots.ai/app/layout.tsx`) includes the `<main>` tag. Therefore, child layouts should avoid using `<main>` to prevent duplication.

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to apps/{web,pro-web}/app/** : Follow Next.js file conventions for routing and layout in the App Router

Applied to files:

  • CLAUDE.md
  • AGENTS.md
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Pro workspace editors: controlled/uncontrolled patterns, Markdown utilities, section state sync, and timed DOM operations

Applied to files:

  • CLAUDE.md
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to packages/mb-genql/** : Use generated types and client from mb-genql for type-safe GraphQL operations

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to packages/mb-genql/** : Use generated types and client from mb-genql for type-safe GraphQL calls

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to **/*.{ts,tsx} : Prefer RORO (Receive an object, return an object) for function signatures, especially for external service interactions

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to **/*.{ts,tsx} : Avoid default exports; prefer named exports

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to **/*.{ts,tsx} : Use meaningful variable names; prefix booleans with is/has/does/should

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to **/*.tsx : Avoid inline arrow functions in JSX props; use stable function references for event handlers

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to **/*.{ts,tsx} : Use lowercase dash-separated names for files and folders; use extensions like .config.ts, .test.ts, .context.tsx, .type.ts, .hook.ts; components may omit extension

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to **/*.{ts,tsx} : Use interface for object shapes/classes; type for unions/tuples/aliases; const for literals; enum for fixed enumerations

Applied to files:

  • CLAUDE.md
  • AGENTS.md
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to **/*.{ts,tsx} : Use interface for objects/classes; type for unions/tuples/aliases; enum for fixed sets; const for literals

Applied to files:

  • CLAUDE.md
  • AGENTS.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Adopt unidirectional data flow: User Input → Component State → Custom Hooks → Server Actions → Database

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to apps/{web,pro-web}/components/ui/**/*.{ts,tsx} : Follow Radix UI patterns under components/ui/

Applied to files:

  • CLAUDE.md
  • AGENTS.md
📚 Learning: 2024-12-05T01:08:50.085Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2024-12-05T01:08:50.085Z
Learning: Test files follow the pattern `*.test.ts` or `*.spec.ts` and should implement the AAA (Arrange-Act-Assert) pattern with proper mocking of external dependencies.

Applied to files:

  • CLAUDE.md
  • AGENTS.md
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Follow unidirectional data flow: User Input → Component State → Custom Hooks → Server Actions → Database

Applied to files:

  • CLAUDE.md
📚 Learning: 2024-12-05T01:19:41.647Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2024-12-05T01:19:41.647Z
Learning: The project follows strict naming conventions: lowercase with dashes for directories/names, and specific extensions (.config.ts, .test.ts, .context.tsx, .type.ts, .hook.ts) for different file types.

Applied to files:

  • CLAUDE.md
  • AGENTS.md
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to **/services/hasura.service.ts : Use hasura.service.ts as the single point for all GraphQL interactions

Applied to files:

  • apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to **/services/hasura.service.ts : Use hasura.service.ts as the single interaction point for GraphQL operations with Hasura

Applied to files:

  • apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: For DOM/Browser work: feature-detect, progressively enhance, use timing (delays/RAF), and verify operations

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: For React/State: minimize useEffect deps, use refs for non-reactive values, separate state vs DOM manipulation

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Follow environment-aware, resilient problem-solving (verify results, add fallbacks, consider timing, add observability, test incrementally)

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to apps/{web,pro-web}/**/*.{ts,tsx} : Verify DOM operations and consider timing (RAF/delays) when manipulating the DOM in React

Applied to files:

  • AGENTS.md
  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Include error handling, fallbacks, verification, observability, and incremental testing in solutions

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Embrace React’s declarative style; avoid imperative DOM manipulation

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to apps/{web,pro-web}/app/** : Follow Next.js file conventions in the App Router

Applied to files:

  • AGENTS.md
📚 Learning: 2024-12-05T01:14:50.365Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2024-12-05T01:14:50.365Z
Learning: The codebase enforces TypeScript strict mode and follows Biome formatting rules across all TypeScript/JavaScript files.

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to **/*.{ts,tsx} : Avoid using any; prefer precise types

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to **/*.{ts,tsx} : Avoid any; prefer specific types

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to **/*.{ts,tsx} : Leverage type inference; omit explicit annotations when obvious

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to **/*.{ts,tsx} : Leverage type inference where clear; annotate function params and return types

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to **/*.{ts,tsx} : Leverage type inference when clear; omit redundant annotations

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to **/*.{ts,tsx} : Type definitions: use interface for objects/classes; type for unions/tuples/aliases; const for literals; enum for fixed sets

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to **/*.{ts,tsx} : Add explicit type annotations for function parameters and return values

Applied to files:

  • AGENTS.md
📚 Learning: 2024-12-05T01:14:50.365Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2024-12-05T01:14:50.365Z
Learning: The project uses shadcn/ui conventions for UI components and follows atomic design principles for component organization.

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to apps/{web,pro-web}/components/shared/**/*.{ts,tsx} : Store reusable UI elements in components/shared/

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to apps/{web,pro-web}/components/ui/** : Follow Radix UI patterns for UI components in components/ui/

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-30T22:09:37.969Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-30T22:09:37.969Z
Learning: Applies to apps/{web,pro-web}/**/*.tsx : Order React component files: imports, constants, component, styled components, types/interfaces

Applied to files:

  • AGENTS.md
📚 Learning: 2025-09-04T21:31:18.830Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2025-09-04T21:31:18.830Z
Learning: The pro-web workspace system has identified performance bottlenecks in markdown parsing, AI streaming response handling, selection range management, Base64 document processing, and multiple debounced state operations that require optimization and better coordination.

Applied to files:

  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-09-04T21:31:18.830Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2025-09-04T21:31:18.830Z
Learning: The pro-web workspace features have complex performance considerations including markdown parsing optimization, AI streaming response handling, state persistence coordination, and selection range management that require careful monitoring and optimization.

Applied to files:

  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-09-04T21:31:18.830Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2025-09-04T21:31:18.830Z
Learning: Edge cases for pro-web workspace include concurrent document editing, AI streaming interruptions, large document handling, selection range boundary conditions, and storage failure scenarios that need comprehensive testing and error handling.

Applied to files:

  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-06-13T04:19:05.145Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 508
File: apps/masterbots.ai/components/shared/markdown.tsx:21-33
Timestamp: 2025-06-13T04:19:05.145Z
Learning: `MemoizedReactMarkdown` is always used with `children` as a plain string (never React nodes) in the masterbots.ai codebase.

Applied to files:

  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-01-21T16:52:47.056Z
Learnt from: Bran18
Repo: bitcashorg/masterbots PR: 337
File: package.json:32-34
Timestamp: 2025-01-21T16:52:47.056Z
Learning: The markdown rendering in this codebase uses `react-markdown` with `remark-gfm` and `remark-math` plugins for standard markdown and GitHub Flavored Markdown support. Raw HTML parsing through `rehype-raw` is not needed.

Applied to files:

  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to apps/{web,pro-web}/app/** : Prefer Next.js Server Components; use 'use client' only when necessary

Applied to files:

  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-09-30T22:08:30.965Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-30T22:08:30.965Z
Learning: Applies to apps/{web,pro-web}/**/*.tsx : Declare React components with the function keyword

Applied to files:

  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to apps/{web,pro-web}/**/*.{ts,tsx} : Use Streaming and Suspense in Next.js 14/15 where appropriate

Applied to files:

  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2024-10-23T07:33:36.041Z
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 287
File: apps/masterbots.ai/lib/hooks/use-thread-visibility.tsx:3-3
Timestamp: 2024-10-23T07:33:36.041Z
Learning: Always use explicit imports for React hooks to maintain consistency across the monorepo, and avoid using both named and default imports simultaneously.

Applied to files:

  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
📚 Learning: 2025-09-30T22:11:18.270Z
Learnt from: CR
Repo: bitcashorg/masterbots PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-30T22:11:18.270Z
Learning: Applies to apps/{web,pro-web}/lib/hooks/**/*.{ts,tsx} : Place custom React hooks in lib/hooks/

Applied to files:

  • apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx
🧬 Code graph analysis (3)
apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts (3)
apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts (1)
  • AssistantAnswerScopeType (32-35)
apps/pro-web/app/actions/chat-memory/index.ts (2)
  • IngestAssistantAnswerArgs (6-6)
  • ingestAssistantAnswer (7-7)
apps/pro-web/app/actions/chat-memory/embedding.ts (3)
  • embedText1536 (10-42)
  • sha256 (44-46)
  • vectorToPgText (48-51)
apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts (2)
apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts (1)
  • AssistantAnswerScopeType (5-8)
apps/pro-web/app/actions/chat-memory/embedding.ts (2)
  • embedText1536 (10-42)
  • vectorToPgText (48-51)
apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx (6)
apps/pro-web/lib/queries/use-document-content.ts (1)
  • useUpdateDocumentContent (168-407)
apps/pro-web/lib/section-tree-utils.ts (1)
  • buildSectionTree (20-33)
apps/pro-web/lib/markdown-utils.ts (2)
  • combineMarkdownSections (365-377)
  • replaceSectionContent (379-405)
apps/pro-web/lib/memoized-markdown-components.tsx (2)
  • p (62-68)
  • memoizedMarkdownComponents (54-229)
apps/pro-web/lib/utils.ts (1)
  • cn (9-11)
apps/pro-web/components/shared/markdown.tsx (1)
  • MemoizedReactMarkdown (21-40)
🪛 ast-grep (0.40.5)
apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts

[warning] 130-132: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
\\$${safeVarName}" of type "[^"]+" used in position expecting type "([^"]+)",
)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (25)
apps/pro-web/components/routes/chat/chat-list/index.tsx (5)

21-22: Good type alias for message consistency.

Using MessagePair['userMessage'] as the canonical message shape ensures type alignment with the pairing logic.


35-44: Good use of dynamic import with SSR disabled.

The DocumentThreadOutline component is appropriately code-split with a loading skeleton, consistent with the existing MessagePairs pattern.


46-74: Interface properly documented with compatibility notes.

The JSDoc explaining the dual outlineTree/outline props for call-site compatibility is helpful. Consider deprecating outline in favor of outlineTree in a future cleanup.


134-145: Good use of isEqual for referential stability.

The isEqual check before setPairs prevents unnecessary re-renders when the computed pairs are structurally identical. This is a sound optimization pattern.


248-274: Clean conditional rendering with maintained parity.

The ternary structure clearly separates the outline and fallback paths. Both renderers receive consistent props which ensures behavioral parity.

apps/pro-web/components/routes/workspace/document-tab/workspace-document-text-editor.tsx (6)

23-72: LGTM!

Component declaration follows guidelines with the function keyword, and the memoized derivations for activeSectionObj and activeSectionContent have correct dependencies.


99-130: LGTM!

The sync effects properly guard against overwriting user edits during remote updates by checking isUserTypingRef.current and comparing with last-synced values.


132-164: LGTM!

Good use of requestAnimationFrame for DOM operations per coding guidelines. The coordinate conversion from absolute to section-relative positions is correctly implemented with clamping.


202-243: LGTM!

The deletion handler robustly matches sections by id or title, provides user confirmation, and properly clears the active section when the deleted section was selected.


245-298: LGTM!

The debounced save handlers correctly clear previous timers before scheduling new ones, and use appropriate utilities (replaceSectionContent for section edits, direct content for source edits).


452-501: LGTM!

Source view mode correctly handles preview and edit states, with debounced saves and absolute selection range tracking.

CLAUDE.md (2)

80-80: Improved section naming.

The rename from "Common Development Commands" to "Setup Commands" is more accurate and helps readers quickly identify installation and initialization procedures.


139-141: Documentation versions are accurate and match dependencies.

The documented versions (Next.js 15, React 19, TypeScript 5.8, Bun 1.x) correctly reflect the actual versions in package.json:

  • apps/pro-web and apps/web: Next.js 15.5.7, React 19.2.1, TypeScript ^5.8.2
  • Root package.json: Bun 1.x specified in engines

No changes needed to the documentation.

README.md (1)

55-58: Bun dev command aligns with repo tooling.

Nice update to keep the frontend instructions consistent with Bun usage.

AGENTS.md (3)

55-78: Implementation guidelines are clear and actionable.

This section fits well with the environment-awareness framing and is easy to apply.


116-231: Monorepo architecture and workflow guidance reads well.

The structure, tech stack, and data-flow notes are concise and helpful.


287-291: TypeScript “avoid any” reminder is a good touch.

Keeps the conventions section crisp and aligned with strict typing.

apps/pro-web/app/actions/chat-memory/embedding.ts (2)

44-46: Hash helper is straightforward.

Clear and deterministic implementation.


48-50: Pgvector literal formatting looks correct.

Exact no-space serialization is the right shape for pgvector.

apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts (2)

5-32: Scope and argument typing looks solid.

The public types are clear and narrow the scope inputs well.


34-45: Normalization helpers are clean and readable.

The scope/type guards are straightforward and safe.

apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts (4)

6-30: Row normalization is robust.

Handles camelCase/snake_case inputs without resorting to any.


71-121: Token budgeting logic looks solid.

Clear defaults, deterministic estimation, and sensible pruning behavior.


123-145: Regex escaping + arg disabling helpers are safe.

Good guardrails for schema-variance error handling.


327-513: Primary + fallback retrieval flow is clear.

The scoped search + doc-root fallback behavior is easy to follow and preserves legacy compatibility.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +10 to +41
export async function embedText1536(input: string): Promise<number[]> {
const apiKey = process.env.OPENAI_API_KEY
const text = (input ?? '').trim()

if (!apiKey) throw new Error('Missing OPENAI_API_KEY')
if (!text) return []

const res = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: EMBEDDING_MODEL,
input: text,
encoding_format: 'float',
}),
// ? We do not store cache for embeddings... that wouldn't be safe and would lead to stale embeddings
cache: 'no-store',
})

if (!res.ok) {
const body = await res.text().catch(() => '')
throw new Error(
`OpenAI embeddings failed: ${res.status}${res.statusText}${body}`,
)
}

const json = (await res.json()) as OpenAIEmbeddingsResponse
const vec = json.data?.[0]?.embedding ?? []
return vec
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

🧩 Analysis chain

🏁 Script executed:

find apps/pro-web/app/actions -name "embedding.ts" -o -name "*embedding*"

Repository: bitcashorg/masterbots

Length of output: 114


🏁 Script executed:

git ls-files | grep -i "embedding"

Repository: bitcashorg/masterbots

Length of output: 114


🏁 Script executed:

find apps -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "AbortController" | head -20

Repository: bitcashorg/masterbots

Length of output: 264


🏁 Script executed:

cat -n apps/pro-web/app/actions/chat-memory/embedding.ts

Repository: bitcashorg/masterbots

Length of output: 1792


🏁 Script executed:

rg "AbortController|setTimeout.*abort" apps --type ts --type tsx -A 3 -B 1

Repository: bitcashorg/masterbots

Length of output: 92


🏁 Script executed:

rg "AbortController|setTimeout.*abort" apps -A 3 -B 1

Repository: bitcashorg/masterbots

Length of output: 3040


🏁 Script executed:

rg "await fetch" apps/pro-web/app/actions -A 5 | head -80

Repository: bitcashorg/masterbots

Length of output: 5943


🏁 Script executed:

rg "signal.*controller" apps -A 2 -B 2

Repository: bitcashorg/masterbots

Length of output: 18794


🏁 Script executed:

rg "setTimeout.*abort|try.*finally.*clearTimeout" apps/pro-web/app/actions -A 5 -B 2

Repository: bitcashorg/masterbots

Length of output: 47


🏁 Script executed:

cat -n apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts | head -50

Repository: bitcashorg/masterbots

Length of output: 1715


🏁 Script executed:

cat -n apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts | sed -n '50,120p'

Repository: bitcashorg/masterbots

Length of output: 2602


🏁 Script executed:

rg "fetch.*openai|fetch.*embeddings" apps -A 10

Repository: bitcashorg/masterbots

Length of output: 897


Add a timeout to the OpenAI embeddings request.

The external fetch call can hang indefinitely and block the server action. Add an AbortController timeout to prevent resource exhaustion.

🛠️ Suggested fix
 export async function embedText1536(input: string): Promise<number[]> {
 	const apiKey = process.env.OPENAI_API_KEY
 	const text = (input ?? '').trim()

 	if (!apiKey) throw new Error('Missing OPENAI_API_KEY')
 	if (!text) return []
+
+	const controller = new AbortController()
+	const timeoutId = setTimeout(() => controller.abort(), 15_000)

-	const res = await fetch('https://api.openai.com/v1/embeddings', {
-		method: 'POST',
-		headers: {
-			'content-type': 'application/json',
-			authorization: `Bearer ${apiKey}`,
-		},
-		body: JSON.stringify({
-			model: EMBEDDING_MODEL,
-			input: text,
-			encoding_format: 'float',
-		}),
-		// ? We do not store cache for embeddings... that wouldn't be safe and would lead to stale embeddings
-		cache: 'no-store',
-	})
+	let res: Response
+	try {
+		res = await fetch('https://api.openai.com/v1/embeddings', {
+			method: 'POST',
+			headers: {
+				'content-type': 'application/json',
+				authorization: `Bearer ${apiKey}`,
+			},
+			body: JSON.stringify({
+				model: EMBEDDING_MODEL,
+				input: text,
+				encoding_format: 'float',
+			}),
+			// ? We do not store cache for embeddings... that wouldn't be safe and would lead to stale embeddings
+			cache: 'no-store',
+			signal: controller.signal,
+		})
+	} finally {
+		clearTimeout(timeoutId)
+	}
📝 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
export async function embedText1536(input: string): Promise<number[]> {
const apiKey = process.env.OPENAI_API_KEY
const text = (input ?? '').trim()
if (!apiKey) throw new Error('Missing OPENAI_API_KEY')
if (!text) return []
const res = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: EMBEDDING_MODEL,
input: text,
encoding_format: 'float',
}),
// ? We do not store cache for embeddings... that wouldn't be safe and would lead to stale embeddings
cache: 'no-store',
})
if (!res.ok) {
const body = await res.text().catch(() => '')
throw new Error(
`OpenAI embeddings failed: ${res.status}${res.statusText}${body}`,
)
}
const json = (await res.json()) as OpenAIEmbeddingsResponse
const vec = json.data?.[0]?.embedding ?? []
return vec
export async function embedText1536(input: string): Promise<number[]> {
const apiKey = process.env.OPENAI_API_KEY
const text = (input ?? '').trim()
if (!apiKey) throw new Error('Missing OPENAI_API_KEY')
if (!text) return []
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 15_000)
let res: Response
try {
res = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: EMBEDDING_MODEL,
input: text,
encoding_format: 'float',
}),
// ? We do not store cache for embeddings... that wouldn't be safe and would lead to stale embeddings
cache: 'no-store',
signal: controller.signal,
})
} finally {
clearTimeout(timeoutId)
}
if (!res.ok) {
const body = await res.text().catch(() => '')
throw new Error(
`OpenAI embeddings failed: ${res.status}${res.statusText}${body}`,
)
}
const json = (await res.json()) as OpenAIEmbeddingsResponse
const vec = json.data?.[0]?.embedding ?? []
return vec
🤖 Prompt for AI Agents
In `@apps/pro-web/app/actions/chat-memory/embedding.ts` around lines 10 - 41,
embedText1536 can hang because the fetch to OpenAI has no timeout; wrap the
fetch in an AbortController with a configurable timeout (e.g., 10s), pass
controller.signal into the fetch call, start a setTimeout that calls
controller.abort() after the timeout, and clear the timer after a successful
response (and on error) so the timer doesn't leak; ensure you catch an abort
error and rethrow a meaningful Error (e.g., "OpenAI embeddings request timed
out") before continuing to parse the response.

Comment on lines +147 to +318
function makeSearchQuery(opts: {
embeddingVarType: string
scopeTypeVarType: string
scopeIdVarType: string
docNodeIdVarType: string
includeScope: boolean
includeDocNode: boolean
includeExclude: boolean
}) {
const varDefs: string[] = [
'$threadId: uuid!',
`$queryEmbedding: ${opts.embeddingVarType}!`,
'$k: Int!',
'$minCosine: float8!',
]
if (opts.includeExclude) varDefs.push('$excludeMessageId: uuid')
if (opts.includeScope) {
varDefs.push(`$scopeType: ${opts.scopeTypeVarType}`)
varDefs.push(`$scopeId: ${opts.scopeIdVarType}`)
}
if (opts.includeDocNode) {
varDefs.push(`$docNodeId: ${opts.docNodeIdVarType}`)
}

const args: string[] = [
'thread_id: $threadId',
'query_embedding: $queryEmbedding',
'k: $k',
'min_cosine: $minCosine',
]
if (opts.includeExclude) args.push('exclude_message_id: $excludeMessageId')
if (opts.includeScope) {
args.push('scope_type: $scopeType')
args.push('scope_id: $scopeId')
}
if (opts.includeDocNode) {
args.push('doc_node_id: $docNodeId')
}

return /* GraphQL */ `
query SearchAssistantAnswers(${varDefs.join(', ')}) {
search_assistant_answers_gql(args: { ${args.join(', ')} }) {
message_id
turn_index
content
cosine
created_at
}
}
`
}

function mergeRowsPreferBestCosine(rows: SearchRow[]): SearchRow[] {
if (!rows.length) return rows
const byId = new Map<string, SearchRow>()
for (const r of rows) {
const prev = byId.get(r.message_id)
if (!prev) {
byId.set(r.message_id, r)
continue
}
// Prefer higher cosine; tie-breaker: higher turn_index
const better =
(r.cosine ?? 0) > (prev.cosine ?? 0) ||
((r.cosine ?? 0) === (prev.cosine ?? 0) &&
(r.turn_index ?? 0) > (prev.turn_index ?? 0))
if (better) byId.set(r.message_id, r)
}
return [...byId.values()]
}

async function searchAssistantAnswers(params: {
threadId: string
queryEmbeddingText: string
k: number
minCosine: number
excludeMessageId?: string | null
scopeType?: string | null
scopeId?: string | null
docNodeId?: string | null
}): Promise<SearchRow[]> {
/**
* We try a “modern” query (scope args + doc_node_id included) and gracefully fall back if:
* - the schema doesn’t support doc_node_id arg yet
* - the schema doesn’t support scope/exclude args
* - enum/scalar types differ
*/
let includeScope = params.scopeType != null
let includeExclude = params.excludeMessageId != null
let includeDocNode = params.docNodeId != null

let embeddingVarType = 'vector'
let scopeTypeVarType = 'String'
let scopeIdVarType = 'String'
let docNodeIdVarType = 'String'

for (let attempt = 0; attempt < 6; attempt++) {
const query = makeSearchQuery({
embeddingVarType,
scopeTypeVarType,
scopeIdVarType,
docNodeIdVarType,
includeScope,
includeDocNode,
includeExclude,
})

try {
type Data = { search_assistant_answers_gql: SearchRow[] }
const data = await hasuraGql<Data>(query, {
threadId: params.threadId,
queryEmbedding: params.queryEmbeddingText,
k: params.k,
minCosine: params.minCosine,
...(includeExclude
? { excludeMessageId: params.excludeMessageId }
: {}),
...(includeScope
? { scopeType: params.scopeType, scopeId: params.scopeId ?? null }
: {}),
...(includeDocNode ? { docNodeId: params.docNodeId } : {}),
})
return data.search_assistant_answers_gql ?? []
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)

// If doc_node_id arg isn’t in the schema, drop it and retry.
if (includeDocNode && shouldDisableArg(msg, 'doc_node_id')) {
includeDocNode = false
continue
}

// If scope/exclude args aren’t in the schema, drop them and retry.
if (includeScope && shouldDisableArg(msg, 'scope_type')) {
includeScope = false
continue
}
if (includeExclude && shouldDisableArg(msg, 'exclude_message_id')) {
includeExclude = false
continue
}

// If GraphQL expects different types for vars, adjust and retry.
const expScopeType = includeScope
? tryExtractExpectedType(msg, 'scopeType')
: null
if (expScopeType) {
scopeTypeVarType = expScopeType
continue
}

const expScopeId = includeScope
? tryExtractExpectedType(msg, 'scopeId')
: null
if (expScopeId) {
scopeIdVarType = expScopeId
continue
}

const expDocNode = includeDocNode
? tryExtractExpectedType(msg, 'docNodeId')
: null
if (expDocNode) {
docNodeIdVarType = expDocNode
continue
}

const expEmbed = tryExtractExpectedType(msg, 'queryEmbedding')
if (expEmbed) {
embeddingVarType = expEmbed
continue
}
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

🧩 Analysis chain

🏁 Script executed:

wc -l apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts

Repository: bitcashorg/masterbots

Length of output: 143


🏁 Script executed:

cat -n apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts

Repository: bitcashorg/masterbots

Length of output: 18622


Add adaptive type handling for minCosine to match the pattern used for queryEmbedding.

Line 160 hardcodes minCosine to float8, but if the Hasura schema expects a different numeric type (e.g., float4), the query fails without fallback. Unlike queryEmbedding (which adapts at lines 314–318), minCosine has no error-driven type adjustment. Implement the same pattern for consistency.

🛠️ Suggested fix
 function makeSearchQuery(opts: {
 	embeddingVarType: string
+	minCosineVarType: string
 	scopeTypeVarType: string
 	scopeIdVarType: string
 	docNodeIdVarType: string
 	includeScope: boolean
 	includeDocNode: boolean
 	includeExclude: boolean
 }) {
 	const varDefs: string[] = [
 		'$threadId: uuid!',
 		`$queryEmbedding: ${opts.embeddingVarType}!`,
 		'$k: Int!',
-		'$minCosine: float8!',
+		`$minCosine: ${opts.minCosineVarType}!`,
 	]
 	let embeddingVarType = 'vector'
+	let minCosineVarType = 'float8'
 	let scopeTypeVarType = 'String'
 	let scopeIdVarType = 'String'
 	let docNodeIdVarType = 'String'

 	for (let attempt = 0; attempt < 6; attempt++) {
 		const query = makeSearchQuery({
 			embeddingVarType,
+			minCosineVarType,
 			scopeTypeVarType,
 			scopeIdVarType,
 			docNodeIdVarType,
 			includeScope,
 			includeDocNode,
 			includeExclude,
 		})
 			const expEmbed = tryExtractExpectedType(msg, 'queryEmbedding')
 			if (expEmbed) {
 				embeddingVarType = expEmbed
 				continue
 			}
+
+			const expMinCosine = tryExtractExpectedType(msg, 'minCosine')
+			if (expMinCosine) {
+				minCosineVarType = expMinCosine
+				continue
+			}
🤖 Prompt for AI Agents
In `@apps/pro-web/app/actions/chat-memory/retrieve-assistant-answer-memories.ts`
around lines 147 - 318, The query currently hardcodes minCosine's GraphQL var
type to "float8" in makeSearchQuery and never adapts on GraphQL type errors;
update makeSearchQuery and searchAssistantAnswers to treat minCosine like
queryEmbedding: add a minCosineVarType parameter (default "float8") to
makeSearchQuery and use it in varDefs (`$minCosine: ${minCosineVarType}!`),
ensure the hasuraGql call still passes minCosine value, and inside the retry
loop call tryExtractExpectedType(msg, 'minCosine') (e.g., expMinCosine) and if
present assign minCosineVarType = expMinCosine and continue so the code retries
with the corrected numeric type; reference functions/identifiers:
makeSearchQuery, searchAssistantAnswers, minCosine, tryExtractExpectedType.

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