Skip to content

feat: Studio Pipeline Hardening — Session Drawer, Gemini v1beta, and Execution Visibility#21

Open
Ker102 wants to merge 5 commits intomainfrom
feat/studio-pipeline-hardening
Open

feat: Studio Pipeline Hardening — Session Drawer, Gemini v1beta, and Execution Visibility#21
Ker102 wants to merge 5 commits intomainfrom
feat/studio-pipeline-hardening

Conversation

@Ker102
Copy link
Owner

@Ker102 Ker102 commented Mar 9, 2026

Studio Pipeline Hardening

Changes

Session Drawer Enhancements

  • Plan execution summary card — displays plan summary, step count, and success/failure status with color-coded indicators
  • Command results list — shows each executed tool with ✅/❌/⏳ status, description, and error details
  • New typesStepPlanData and StepCommandResult for structured execution data
  • Complete event handler — extracts planning metadata and commandSuggestions from API response

Gemini API Fixes

  • Switched API endpoint from v1 to v1beta (required for preview models)
  • Updated model to gemini-3.1-pro-preview

Studio Mode Fix

  • Changed workflowMode from \"studio\" to \"autopilot\" when executing steps
  • Studio mode previously only proposed workflows without executing — now triggers planner + executor pipeline

Files Changed

  • components/projects/workflow-timeline.tsx — Added StepPlanData, StepCommandResult types
  • components/projects/studio-layout.tsx — Updated complete event handler, fixed workflowMode
  • components/projects/step-session-drawer.tsx — Added plan summary and command results display
  • lib/gemini.ts — v1beta endpoint + model update
  • lib/ai/index.ts — Model constant update

Testing

  • Studio mode end-to-end test with Blender agent
  • Verify plan data appears in session drawer after execution
  • Confirm Gemini API responds with v1beta endpoint

Summary by CodeRabbit

  • New Features

    • Plan Summary block in step details showing success/failure, step count, summary, and errors.
    • Executed Commands list in step details with per-command status, tool, descriptions, and errors.
    • Post-run follow-up text generated and streamed into assistant responses.
  • Updates

    • Execution mode switched to autopilot for richer results.
    • AI endpoint and default model updated.
    • Local dev startup now derives port for cookie-clearing origins and logs.
    • Screenshot tool now accepts a single maxSize parameter.

…ults

- Add StepPlanData and StepCommandResult types to workflow-timeline.tsx
- Extract planning metadata and commandSuggestions from complete event
- Display plan execution summary card (success/fail, step count, errors)
- Render executed command results with status indicators
- Fix workflowMode: autopilot (not studio) for real execution
- Switch Gemini to v1beta endpoint + gemini-3.1-pro-preview model
@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

Adds execution planning and per-command results to workflow steps, runs planner+executor in autopilot mode and records planData/commandResults (with a follow-up LLM pass), updates Gemini defaults and embedding endpoints, changes viewport screenshot API to use maxSize with a file fallback, and surfaces Plan Summary / Executed Commands in the UI.

Changes

Cohort / File(s) Summary
Workflow Data Types
components/projects/workflow-timeline.tsx
Added exported interfaces StepCommandResult and StepPlanData; extended WorkflowTimelineStep with `planData?: StepPlanData
Step Completion & Autopilot Flow
components/projects/studio-layout.tsx, app/api/ai/chat/route.ts
Switches workflowMode to "autopilot"; on complete extract and attach planData (summary, stepCount, executionSuccess, errors) and commandResults; adds a post-execution follow-up LLM generation step (non-fatal fallback).
Streaming Followup Handling
components/projects/project-chat.tsx
Adds handling for new streaming event followup_delta, appending follow-up text to the assistant message during streaming.
Step Session UI
components/projects/step-session-drawer.tsx
Adds conditional "Plan Summary" and "Executed Commands" UI blocks rendering step.planData and step.commandResults (status badges, summaries, errors, per-command details). Duplicate blocks appear in diff.
Screenshot API & Orchestration
lib/mcp/screenshot.ts, lib/orchestration/executor.ts, lib/ai/prompts.ts
Replaced width/height with maxSize/max_size in public API and MCP requests; added temporary-file workflow for screenshots, inline-or-file fallback, and normalizeParameters conversion from legacy width/height to max_size.
Gemini / Embeddings & Client
lib/ai/index.ts, lib/gemini.ts, lib/ai/embeddings.ts
Changed Gemini default model fallback to gemini-3.1-pro-preview and endpoint to v1beta; buildContents now may insert a model ack; embeddings switched to Gemini REST endpoints (gemini-embedding-001) and use direct fetch with GEMINI_API_KEY.
MCP / MCP Success Logic
lib/ai/agents.ts, lib/mcp/...
Tightened MCP success validation to check nested result.error before trusting top-level status; isMcpSuccess logic reordered to prefer inner errors.
Vectorstore / Prisma
lib/ai/vectorstore.ts
Introduced Prisma.raw/Prisma.sql vector literal construction (VECTOR_CAST, vectorLiteral) and replaced inlined embedding SQL with safe raw literals.
Dev Startup Port
desktop/main.js
In dev mode, derive port from PORT or DEFAULT_PORT for cookie-clearing origins and include port in log output.
Docs & Logs
GEMINI.md, ingestion_log.txt, debug_error.json
Major rewrite of GEMINI.md to operational notes; expanded ingestion log and updated debug error payload to Gemini-style error structure.
Other
components/projects/..., lib/...
Various related updates across UI, orchestration, and ai helper files to support new plan/command flows, follow-up streaming, and screenshot/file handling.

Sequence Diagram

sequenceDiagram
    participant Studio as Studio Layout
    participant Gemini as Gemini API
    participant ChatAPI as Chat Route
    participant Timeline as Workflow Timeline
    participant Drawer as Step Session Drawer

    Studio->>Gemini: Start plan+execute (workflowMode: "autopilot")
    Gemini-->>Studio: Stream events (deltas, command events, complete, followup_delta)
    Studio->>ChatAPI: On complete -> request follow-up LLM summary
    ChatAPI-->>Studio: Return follow-up text (or fallback)
    Studio->>Timeline: Update step with planData & commandResults (mark done)
    Timeline-->>Drawer: Provide updated step
    Drawer->>Drawer: Render Plan Summary (if planData)
    Drawer->>Drawer: Render Executed Commands (if commandResults)
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibbled logs and hopped through code,

Plans and commands in tidy rowed.
Autopilot hummed, the follow-up learned,
Badges gleam where steps were turned.
A joyful hop — execution confirmed!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the three main changes: session drawer enhancements, Gemini API upgrade to v1beta, and improved execution visibility through structured data types.
Description check ✅ Passed The description covers main changes, motivation, files affected, and testing checklist, though testing items are marked incomplete and some implementation details from the changeset are not mentioned.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/studio-pipeline-hardening

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/projects/step-session-drawer.tsx`:
- Around line 294-297: The cmd.error output in
components/projects/step-session-drawer.tsx is being clipped by the "truncate"
class and max-w style so long failure text is invisible; remove the "truncate"
class and inline max-w style from the span rendering cmd.error and instead allow
wrapping (e.g., use break-words / whitespace-pre-wrap) and add an expansion
affordance: implement a per-command toggle in the StepSessionDrawer render
(track by cmd.id or index) that on click opens the full error either inline as a
<pre>-style block or in a small modal/popover so the complete cmd.error content
is readable and copyable. Ensure the changed span no longer truncates and that
the expansion UI shows the raw cmd.error text.

In `@components/projects/studio-layout.tsx`:
- Around line 163-190: The handler currently always calls updateStep(stepId, {
status: "done", ... }) even when the planning metadata indicates failure; change
it to derive the status from planning.executionSuccess (e.g., set status to
"done" when planning.executionSuccess === true, otherwise set status to "failed"
or an appropriate error state), using the existing planning/planData variables
and leaving other fields (planData, commandResults) unchanged when calling
updateStep.
- Around line 176-184: The mapping of event.commandSuggestions into
commandResults trusts raw values: ensure you normalize and validate fields
before storing by (1) validating status from event.commandSuggestions against
the allowed StepCommandResult["status"] union (e.g., map unknown values to
"pending" or a safe default) when constructing status for each StepCommandResult
and (2) guaranteeing a stable, non-empty unique id for each command (do not use
String(cmd.id ?? "") directly—generate or derive a unique id when cmd.id is
missing/empty, e.g., use a prefixed index or UUID) and also coerce
confidence/description/error safely; update the mapping that produces
commandResults (the rawCommands -> commandResults block) to apply these checks
so drawer row keys and displayed statuses cannot be corrupted by malformed API
payloads.

In `@lib/ai/index.ts`:
- Line 18: DEFAULT_MODEL currently falls back to "gemini-3.1-pro-preview",
causing createGeminiModel (and downstream functions generatePlan, validateStep,
generateRecovery, generateCode) to use the wrong LLM; change the fallback value
so DEFAULT_MODEL = process.env.GEMINI_MODEL ?? "gemini-2.5-pro" (Gemini 2.5 Pro)
so createGeminiModel and all chain functions use Gemini 2.5 Pro by default.

In `@lib/gemini.ts`:
- Line 4: DEFAULT_MODEL is set to "gemini-3.1-pro-preview" but
generateGeminiResponse and streamGeminiResponse expect the repo default to be
Gemini 2.5 Pro; change the DEFAULT_MODEL constant to "gemini-2.5-pro" so both
functions fallback to the correct model, and run tests/lint to ensure no other
places rely on the preview value (update any docstring/comments referencing the
old default if present).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4c817bcb-bc73-4f3d-a425-bac529bdf431

📥 Commits

Reviewing files that changed from the base of the PR and between 582a636 and 8b24b09.

📒 Files selected for processing (5)
  • components/projects/step-session-drawer.tsx
  • components/projects/studio-layout.tsx
  • components/projects/workflow-timeline.tsx
  • lib/ai/index.ts
  • lib/gemini.ts

Comment on lines +215 to +303
{/* Plan Summary */}
{step.planData && (
<div
className="rounded-xl border px-4 py-3 space-y-2"
style={{
borderColor: step.planData.executionSuccess
? "hsl(153 60% 53% / 0.3)"
: "hsl(0 84% 60% / 0.3)",
backgroundColor: step.planData.executionSuccess
? "rgba(52,211,153,0.08)"
: "rgba(248,113,113,0.08)",
}}
>
<div className="flex items-center justify-between">
<span
className="text-xs font-semibold uppercase tracking-wider"
style={{ color: "hsl(var(--forge-text-subtle))" }}
>
Execution Plan
</span>
<span
className="text-xs font-medium px-2 py-0.5 rounded-full"
style={{
backgroundColor: step.planData.executionSuccess
? "rgba(52,211,153,0.2)"
: "rgba(248,113,113,0.2)",
color: step.planData.executionSuccess
? "hsl(153 60% 53%)"
: "hsl(0 84% 60%)",
}}
>
{step.planData.executionSuccess ? "✓ Success" : "✕ Failed"} · {step.planData.stepCount} step{step.planData.stepCount !== 1 ? "s" : ""}
</span>
</div>
<p className="text-sm" style={{ color: "hsl(var(--forge-text))" }}>
{step.planData.planSummary}
</p>
{step.planData.errors && step.planData.errors.length > 0 && (
<div className="text-xs space-y-1" style={{ color: "hsl(0 84% 60%)" }}>
{step.planData.errors.map((err, i) => (
<p key={i}>⚠ {err}</p>
))}
</div>
)}
</div>
)}

{/* Command Results */}
{step.commandResults && step.commandResults.length > 0 && (
<div className="space-y-1.5">
<span
className="text-xs font-semibold uppercase tracking-wider"
style={{ color: "hsl(var(--forge-text-subtle))" }}
>
Executed Commands ({step.commandResults.length})
</span>
<div className="space-y-1">
{step.commandResults.map((cmd) => (
<div
key={cmd.id}
className="flex items-center gap-2 rounded-lg px-3 py-2 text-xs font-mono"
style={{
backgroundColor: "hsl(var(--forge-surface-dim))",
border: "1px solid hsl(var(--forge-border))",
color: "hsl(var(--forge-text))",
}}
>
<span className="shrink-0">
{cmd.status === "executed" ? "✅" : cmd.status === "failed" ? "❌" : "⏳"}
</span>
<span className="truncate flex-1">{cmd.tool}</span>
{cmd.description && (
<span
className="text-[10px] truncate max-w-[40%]"
style={{ color: "hsl(var(--forge-text-subtle))" }}
>
{cmd.description}
</span>
)}
{cmd.error && (
<span className="text-[10px] truncate max-w-[40%]" style={{ color: "hsl(0 84% 60%)" }}>
{cmd.error}
</span>
)}
</div>
))}
</div>
</div>
)}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Move these new state styles into Tailwind/theme classes.

This block adds a large new batch of inline style props for borders, backgrounds, and text colors. That keeps the drawer harder to theme and is out of line with the component styling rule used elsewhere in the repo.

As per coding guidelines, "Use Tailwind CSS utility classes for styling instead of inline styles or CSS files".

Comment on lines +294 to +297
{cmd.error && (
<span className="text-[10px] truncate max-w-[40%]" style={{ color: "hsl(0 84% 60%)" }}>
{cmd.error}
</span>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t hide command failures behind truncate.

The whole point of this drawer change is execution visibility, but long cmd.error values are cut off with no expansion path. That makes the per-command failure details non-actionable in exactly the scenarios this UI is meant to debug.

Suggested fix
-                                    {cmd.error && (
-                                        <span className="text-[10px] truncate max-w-[40%]" style={{ color: "hsl(0 84% 60%)" }}>
-                                            {cmd.error}
-                                        </span>
-                                    )}
+                                    {cmd.error && (
+                                        <span className="text-[10px] max-w-[40%] whitespace-pre-wrap break-words text-red-400">
+                                            {cmd.error}
+                                        </span>
+                                    )}
📝 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
{cmd.error && (
<span className="text-[10px] truncate max-w-[40%]" style={{ color: "hsl(0 84% 60%)" }}>
{cmd.error}
</span>
{cmd.error && (
<span className="text-[10px] max-w-[40%] whitespace-pre-wrap break-words text-red-400">
{cmd.error}
</span>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/projects/step-session-drawer.tsx` around lines 294 - 297, The
cmd.error output in components/projects/step-session-drawer.tsx is being clipped
by the "truncate" class and max-w style so long failure text is invisible;
remove the "truncate" class and inline max-w style from the span rendering
cmd.error and instead allow wrapping (e.g., use break-words /
whitespace-pre-wrap) and add an expansion affordance: implement a per-command
toggle in the StepSessionDrawer render (track by cmd.id or index) that on click
opens the full error either inline as a <pre>-style block or in a small
modal/popover so the complete cmd.error content is readable and copyable. Ensure
the changed span no longer truncates and that the expansion UI shows the raw
cmd.error text.

Comment on lines +163 to +190
// Extract plan data from the planning metadata
const planning = event.planning as Record<string, unknown> | undefined
let planData: StepPlanData | null = null
if (planning) {
planData = {
planSummary: typeof planning.planSummary === "string" ? planning.planSummary : "No summary",
stepCount: Array.isArray(planning.planSteps) ? planning.planSteps.length : 0,
executionSuccess: planning.executionSuccess === true,
errors: Array.isArray(planning.errors) ? planning.errors as string[] : undefined,
}
}

// Extract command results
const rawCommands = Array.isArray(event.commandSuggestions) ? event.commandSuggestions as Record<string, unknown>[] : []
const commandResults: StepCommandResult[] = rawCommands.map((cmd) => ({
id: String(cmd.id ?? ""),
tool: String(cmd.tool ?? ""),
status: (cmd.status as StepCommandResult["status"]) ?? "pending",
confidence: typeof cmd.confidence === "number" ? cmd.confidence : undefined,
description: typeof cmd.description === "string" ? cmd.description : undefined,
error: typeof cmd.error === "string" ? cmd.error : undefined,
}))

updateStep(stepId, {
status: "done",
planData,
commandResults: commandResults.length > 0 ? commandResults : undefined,
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Failed autopilot runs are still being marked as done.

The complete handler always writes status: "done", but the API explicitly sends planning.executionSuccess = false for unsuccessful executions. That means the timeline pill and drawer header can say “Complete” while the new plan summary card says the run failed.

Suggested fix
                                 if (planning) {
                                     planData = {
                                         planSummary: typeof planning.planSummary === "string" ? planning.planSummary : "No summary",
                                         stepCount: Array.isArray(planning.planSteps) ? planning.planSteps.length : 0,
                                         executionSuccess: planning.executionSuccess === true,
                                         errors: Array.isArray(planning.errors) ? planning.errors as string[] : undefined,
                                     }
                                 }

                                 // Extract command results
                                 const rawCommands = Array.isArray(event.commandSuggestions) ? event.commandSuggestions as Record<string, unknown>[] : []
                                 const commandResults: StepCommandResult[] = rawCommands.map((cmd) => ({
                                     id: String(cmd.id ?? ""),
                                     tool: String(cmd.tool ?? ""),
                                     status: (cmd.status as StepCommandResult["status"]) ?? "pending",
                                     confidence: typeof cmd.confidence === "number" ? cmd.confidence : undefined,
                                     description: typeof cmd.description === "string" ? cmd.description : undefined,
                                     error: typeof cmd.error === "string" ? cmd.error : undefined,
                                 }))
+                                const hasCommandFailure = commandResults.some((cmd) => cmd.status === "failed")
+                                const executionSucceeded = planData?.executionSuccess !== false && !hasCommandFailure

                                 updateStep(stepId, {
-                                    status: "done",
+                                    status: executionSucceeded ? "done" : "failed",
                                     planData,
                                     commandResults: commandResults.length > 0 ? commandResults : undefined,
+                                    error: executionSucceeded ? undefined : planData?.errors?.[0],
                                 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/projects/studio-layout.tsx` around lines 163 - 190, The handler
currently always calls updateStep(stepId, { status: "done", ... }) even when the
planning metadata indicates failure; change it to derive the status from
planning.executionSuccess (e.g., set status to "done" when
planning.executionSuccess === true, otherwise set status to "failed" or an
appropriate error state), using the existing planning/planData variables and
leaving other fields (planData, commandResults) unchanged when calling
updateStep.

Comment on lines +176 to +184
const rawCommands = Array.isArray(event.commandSuggestions) ? event.commandSuggestions as Record<string, unknown>[] : []
const commandResults: StepCommandResult[] = rawCommands.map((cmd) => ({
id: String(cmd.id ?? ""),
tool: String(cmd.tool ?? ""),
status: (cmd.status as StepCommandResult["status"]) ?? "pending",
confidence: typeof cmd.confidence === "number" ? cmd.confidence : undefined,
description: typeof cmd.description === "string" ? cmd.description : undefined,
error: typeof cmd.error === "string" ? cmd.error : undefined,
}))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize commandSuggestions before storing them.

This mapping trusts raw API data in two places: cmd.status as StepCommandResult["status"] accepts arbitrary strings as valid union members, and String(cmd.id ?? "") can produce duplicate empty IDs. The drawer keys rows by cmd.id, so malformed payloads can lead to unstable rendering and misleading statuses.

Suggested fix
-                                const rawCommands = Array.isArray(event.commandSuggestions) ? event.commandSuggestions as Record<string, unknown>[] : []
-                                const commandResults: StepCommandResult[] = rawCommands.map((cmd) => ({
-                                    id: String(cmd.id ?? ""),
-                                    tool: String(cmd.tool ?? ""),
-                                    status: (cmd.status as StepCommandResult["status"]) ?? "pending",
-                                    confidence: typeof cmd.confidence === "number" ? cmd.confidence : undefined,
-                                    description: typeof cmd.description === "string" ? cmd.description : undefined,
-                                    error: typeof cmd.error === "string" ? cmd.error : undefined,
-                                }))
+                                const rawCommands = Array.isArray(event.commandSuggestions)
+                                    ? event.commandSuggestions as Record<string, unknown>[]
+                                    : []
+                                const allowedStatuses: ReadonlySet<StepCommandResult["status"]> = new Set([
+                                    "pending",
+                                    "ready",
+                                    "executed",
+                                    "failed",
+                                ])
+                                const commandResults: StepCommandResult[] = rawCommands.map((cmd, index) => {
+                                    const rawStatus = typeof cmd.status === "string" ? cmd.status : undefined
+                                    const status: StepCommandResult["status"] =
+                                        rawStatus && allowedStatuses.has(rawStatus as StepCommandResult["status"])
+                                            ? rawStatus as StepCommandResult["status"]
+                                            : "pending"
+
+                                    return {
+                                        id: typeof cmd.id === "string" && cmd.id.length > 0 ? cmd.id : `${stepId}-cmd-${index}`,
+                                        tool: typeof cmd.tool === "string" ? cmd.tool : "unknown",
+                                        status,
+                                        confidence: typeof cmd.confidence === "number" ? cmd.confidence : undefined,
+                                        description: typeof cmd.description === "string" ? cmd.description : undefined,
+                                        error: typeof cmd.error === "string" ? cmd.error : undefined,
+                                    }
+                                })
📝 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 rawCommands = Array.isArray(event.commandSuggestions) ? event.commandSuggestions as Record<string, unknown>[] : []
const commandResults: StepCommandResult[] = rawCommands.map((cmd) => ({
id: String(cmd.id ?? ""),
tool: String(cmd.tool ?? ""),
status: (cmd.status as StepCommandResult["status"]) ?? "pending",
confidence: typeof cmd.confidence === "number" ? cmd.confidence : undefined,
description: typeof cmd.description === "string" ? cmd.description : undefined,
error: typeof cmd.error === "string" ? cmd.error : undefined,
}))
const rawCommands = Array.isArray(event.commandSuggestions)
? event.commandSuggestions as Record<string, unknown>[]
: []
const allowedStatuses: ReadonlySet<StepCommandResult["status"]> = new Set([
"pending",
"ready",
"executed",
"failed",
])
const commandResults: StepCommandResult[] = rawCommands.map((cmd, index) => {
const rawStatus = typeof cmd.status === "string" ? cmd.status : undefined
const status: StepCommandResult["status"] =
rawStatus && allowedStatuses.has(rawStatus as StepCommandResult["status"])
? rawStatus as StepCommandResult["status"]
: "pending"
return {
id: typeof cmd.id === "string" && cmd.id.length > 0 ? cmd.id : `${stepId}-cmd-${index}`,
tool: typeof cmd.tool === "string" ? cmd.tool : "unknown",
status,
confidence: typeof cmd.confidence === "number" ? cmd.confidence : undefined,
description: typeof cmd.description === "string" ? cmd.description : undefined,
error: typeof cmd.error === "string" ? cmd.error : undefined,
}
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/projects/studio-layout.tsx` around lines 176 - 184, The mapping of
event.commandSuggestions into commandResults trusts raw values: ensure you
normalize and validate fields before storing by (1) validating status from
event.commandSuggestions against the allowed StepCommandResult["status"] union
(e.g., map unknown values to "pending" or a safe default) when constructing
status for each StepCommandResult and (2) guaranteeing a stable, non-empty
unique id for each command (do not use String(cmd.id ?? "") directly—generate or
derive a unique id when cmd.id is missing/empty, e.g., use a prefixed index or
UUID) and also coerce confidence/description/error safely; update the mapping
that produces commandResults (the rawCommands -> commandResults block) to apply
these checks so drawer row keys and displayed statuses cannot be corrupted by
malformed API payloads.

// ============================================================================

const DEFAULT_MODEL = process.env.GEMINI_MODEL ?? "gemini-3.1-pro-preview-customtools"
const DEFAULT_MODEL = process.env.GEMINI_MODEL ?? "gemini-3.1-pro-preview"
Copy link

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:

git ls-files | head -20

Repository: Ker102/ModelForge

Length of output: 668


🏁 Script executed:

find . -name "index.ts" -o -name "chains.ts" | grep -E "(lib/ai|app)" | head -10

Repository: Ker102/ModelForge

Length of output: 97


🏁 Script executed:

find . -path "*/lib/ai/*" -o -path "*/app/*" | grep -E "\.(ts|tsx|js)$" | head -20

Repository: Ker102/ModelForge

Length of output: 744


🏁 Script executed:

cat -n lib/ai/index.ts

Repository: Ker102/ModelForge

Length of output: 3909


🏁 Script executed:

cat -n lib/ai/chains.ts 2>/dev/null || echo "File not found, searching for chains..."

Repository: Ker102/ModelForge

Length of output: 10318


🏁 Script executed:

fd "chains" --type f

Repository: Ker102/ModelForge

Length of output: 77


🏁 Script executed:

rg "createGeminiModel" --type ts -l

Repository: Ker102/ModelForge

Length of output: 186


🏁 Script executed:

rg "DEFAULT_MODEL" --type ts -A 2 -B 2

Repository: Ker102/ModelForge

Length of output: 1624


🏁 Script executed:

rg "gemini-2.5-pro\|gemini-3.1-pro" --type ts

Repository: Ker102/ModelForge

Length of output: 43


🏁 Script executed:

rg "GEMINI_MODEL" --type ts -B 2 -A 2

Repository: Ker102/ModelForge

Length of output: 845


🏁 Script executed:

rg "gemini" --type ts | grep -i "pro\|model" | head -20

Repository: Ker102/ModelForge

Length of output: 587


🏁 Script executed:

cat lib/ai/index.ts | head -10

Repository: Ker102/ModelForge

Length of output: 318


Use Gemini 2.5 Pro as the primary LLM model, not gemini-3.1-pro-preview

The DEFAULT_MODEL fallback at line 18 causes all chain functions (generatePlan, validateStep, generateRecovery, generateCode) to default to gemini-3.1-pro-preview since none of them pass an explicit model parameter to createGeminiModel(). This violates the requirement to use Gemini 2.5 Pro for all AI operations.

Suggested fix
-const DEFAULT_MODEL = process.env.GEMINI_MODEL ?? "gemini-3.1-pro-preview"
+const DEFAULT_MODEL = process.env.GEMINI_MODEL ?? "gemini-2.5-pro"
📝 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 DEFAULT_MODEL = process.env.GEMINI_MODEL ?? "gemini-3.1-pro-preview"
const DEFAULT_MODEL = process.env.GEMINI_MODEL ?? "gemini-2.5-pro"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ai/index.ts` at line 18, DEFAULT_MODEL currently falls back to
"gemini-3.1-pro-preview", causing createGeminiModel (and downstream functions
generatePlan, validateStep, generateRecovery, generateCode) to use the wrong
LLM; change the fallback value so DEFAULT_MODEL = process.env.GEMINI_MODEL ??
"gemini-2.5-pro" (Gemini 2.5 Pro) so createGeminiModel and all chain functions
use Gemini 2.5 Pro by default.

const GEMINI_API_ENDPOINT = "https://generativelanguage.googleapis.com/v1"
const DEFAULT_MODEL = "gemini-3.1-pro-preview-customtools"
const GEMINI_API_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta"
const DEFAULT_MODEL = "gemini-3.1-pro-preview"
Copy link

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:

# Check the current state of lib/gemini.ts around the DEFAULT_MODEL
cat -n lib/gemini.ts | head -20

Repository: Ker102/ModelForge

Length of output: 673


🏁 Script executed:

# Search for DEFAULT_MODEL usage in the file
rg -n "DEFAULT_MODEL" lib/gemini.ts -A 3 -B 3

Repository: Ker102/ModelForge

Length of output: 883


🏁 Script executed:

# Check the function implementations that use DEFAULT_MODEL
rg -n "generateGeminiResponse|streamGeminiResponse" lib/gemini.ts -A 10

Repository: Ker102/ModelForge

Length of output: 804


🏁 Script executed:

# Search for all usages of these functions across the codebase
rg -n "generateGeminiResponse|streamGeminiResponse" --type ts --type tsx

Repository: Ker102/ModelForge

Length of output: 88


🏁 Script executed:

# Search for function usages with ts files only
rg "generateGeminiResponse|streamGeminiResponse" -t ts --type-add 'ts:*.ts' --type-add 'tsx:*.tsx'

Repository: Ker102/ModelForge

Length of output: 774


🏁 Script executed:

# Alternative: search for imports of these functions
rg "from.*gemini" -t ts --type-add 'ts:*.ts'

Repository: Ker102/ModelForge

Length of output: 274


🏁 Script executed:

# Check lib/llm/index.ts mentioned in the scratchpad
head -200 lib/llm/index.ts | tail -50

Repository: Ker102/ModelForge

Length of output: 1259


🏁 Script executed:

# Search around line 177-191 in llm/index.ts
sed -n '170,200p' lib/llm/index.ts

Repository: Ker102/ModelForge

Length of output: 788


🏁 Script executed:

# Check if there are any references to gemini-2.5-pro anywhere in the codebase
rg "gemini-2.5" -i

Repository: Ker102/ModelForge

Length of output: 502


🏁 Script executed:

# Check for any environment variable documentation or setup files
rg "GEMINI_MODEL" -i -B 2 -A 2

Repository: Ker102/ModelForge

Length of output: 3184


🏁 Script executed:

# Check if there's a .env or config file with model references
fd -e env -e example -e sample

Repository: Ker102/ModelForge

Length of output: 43


🏁 Script executed:

# Search for any documentation about model selection
fd -e md | xargs rg -l "gemini|model" -i

Repository: Ker102/ModelForge

Length of output: 815


Keep Gemini 2.5 Pro as the default model.

Both generateGeminiResponse() and streamGeminiResponse() use DEFAULT_MODEL as a fallback when GEMINI_MODEL env var is not set. Using "gemini-3.1-pro-preview" as the default conflicts with the repo requirement to use Gemini 2.5 Pro as the primary model for all AI operations, making a preview model the baseline for all callers that don't explicitly override it.

Suggested fix
-const DEFAULT_MODEL = "gemini-3.1-pro-preview"
+const DEFAULT_MODEL = "gemini-2.5-pro"
📝 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 DEFAULT_MODEL = "gemini-3.1-pro-preview"
const DEFAULT_MODEL = "gemini-2.5-pro"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/gemini.ts` at line 4, DEFAULT_MODEL is set to "gemini-3.1-pro-preview"
but generateGeminiResponse and streamGeminiResponse expect the repo default to
be Gemini 2.5 Pro; change the DEFAULT_MODEL constant to "gemini-2.5-pro" so both
functions fallback to the correct model, and run tests/lint to ensure no other
places rely on the preview value (update any docstring/comments referencing the
old default if present).

The cookie-clearing logic was hardcoded to port 3000, but the PORT env var
can override the default. This caused Electron to clear cookies for the
wrong origin when PORT=8081 was set in the environment.
- Fix get_viewport_screenshot prompt: use max_size instead of invalid width/height
- Fix screenshot.ts: pass max_size to MCP instead of width/height
- Fix executor.ts: defensive normalization strips hallucinated width/height params
- Add post-execution LLM summary/follow-up generation in chat route
- Wire followup_delta event handler in studio-layout.tsx and project-chat.tsx
- Update gemini.md with current progress
Copy link

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/ai/chat/route.ts`:
- Around line 752-755: The saved assistant message is replacing earlier streamed
content because assistantText is overwritten with followUpText.trim(); instead,
append the follow-up to preserve the initial content: update the logic around
assistantText and followUpText so that if followUpText.trim() is non-empty you
set assistantText = (assistantText || "").trimEnd() + "\n\n" +
followUpText.trim() (or similar spacing), ensuring you preserve any existing
assistantText and only add the follow-up; adjust any trimming to avoid
duplicating whitespace.
- Around line 740-750: The follow-up LLM stream (streamLlmResponse) loop that
builds followUpText does not collect token usage, so its consumption isn't added
to the overall tokenUsage; modify the follow-up call to capture and aggregate
token counts (e.g., maintain a followUpTokenUsage object or numbers and, for
each chunk or final result from streamLlmResponse, add chunk.tokenUsage or the
returned token counts into the existing tokenUsage totals used later for
logging/billing), updating variables referenced here (streamLlmResponse,
followUpText, and the tokenUsage aggregate) so the follow-up call's tokens are
included in the final usage/billing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2730081c-0179-4f60-9639-8bc7a93f0f20

📥 Commits

Reviewing files that changed from the base of the PR and between a8024d8 and b086cfe.

📒 Files selected for processing (6)
  • app/api/ai/chat/route.ts
  • components/projects/project-chat.tsx
  • components/projects/studio-layout.tsx
  • lib/ai/prompts.ts
  • lib/mcp/screenshot.ts
  • lib/orchestration/executor.ts

Comment on lines +740 to +750
for await (const chunk of streamLlmResponse(llmProvider, {
history: [],
messages: [{ role: "user", content: summaryPromptText }],
maxOutputTokens: 256,
systemPrompt: "You are ModelForge, a helpful Blender assistant. Respond conversationally. Do NOT use markdown headers. Keep your response to 2-4 sentences.",
})) {
if (chunk.textDelta) {
followUpText += chunk.textDelta
send({ type: "followup_delta", delta: chunk.textDelta })
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Token usage from the follow-up LLM call is not tracked.

The secondary streamLlmResponse call for follow-up generation doesn't capture or aggregate token usage. This creates a gap in usage accounting — the tokenUsage logged at line 779 and billed at line 841 only reflects the primary call.

Suggested fix — accumulate follow-up tokens
             let followUpText = ""
+            let followUpUsage: typeof tokenUsage | undefined
             for await (const chunk of streamLlmResponse(llmProvider, {
               history: [],
               messages: [{ role: "user", content: summaryPromptText }],
               maxOutputTokens: 256,
               systemPrompt: "You are ModelForge, a helpful Blender assistant. Respond conversationally. Do NOT use markdown headers. Keep your response to 2-4 sentences.",
             })) {
               if (chunk.textDelta) {
                 followUpText += chunk.textDelta
                 send({ type: "followup_delta", delta: chunk.textDelta })
               }
+              if (chunk.usage) {
+                followUpUsage = chunk.usage
+              }
             }
+
+            // Merge follow-up usage into total
+            if (followUpUsage && tokenUsage) {
+              tokenUsage = {
+                promptTokens: (tokenUsage.promptTokens ?? 0) + (followUpUsage.promptTokens ?? 0),
+                responseTokens: (tokenUsage.responseTokens ?? 0) + (followUpUsage.responseTokens ?? 0),
+                totalTokens: (tokenUsage.totalTokens ?? 0) + (followUpUsage.totalTokens ?? 0),
+              }
+            } else if (followUpUsage) {
+              tokenUsage = followUpUsage
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/ai/chat/route.ts` around lines 740 - 750, The follow-up LLM stream
(streamLlmResponse) loop that builds followUpText does not collect token usage,
so its consumption isn't added to the overall tokenUsage; modify the follow-up
call to capture and aggregate token counts (e.g., maintain a followUpTokenUsage
object or numbers and, for each chunk or final result from streamLlmResponse,
add chunk.tokenUsage or the returned token counts into the existing tokenUsage
totals used later for logging/billing), updating variables referenced here
(streamLlmResponse, followUpText, and the tokenUsage aggregate) so the follow-up
call's tokens are included in the final usage/billing.

Comment on lines +752 to +755
// Append follow-up to the assistant text so it's saved to DB
if (followUpText.trim()) {
assistantText = followUpText.trim()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Initial assistant content is discarded when follow-up is generated.

The code replaces assistantText entirely with followUpText.trim() instead of appending. This means the initial streamed content (from lines 511-514) is lost when the message is persisted to the database. Users may see different content during streaming vs. what's stored.

Suggested fix — append instead of replace
             // Append follow-up to the assistant text so it's saved to DB
             if (followUpText.trim()) {
-              assistantText = followUpText.trim()
+              assistantText += "\n\n" + followUpText.trim()
             }
📝 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
// Append follow-up to the assistant text so it's saved to DB
if (followUpText.trim()) {
assistantText = followUpText.trim()
}
// Append follow-up to the assistant text so it's saved to DB
if (followUpText.trim()) {
assistantText += "\n\n" + followUpText.trim()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/ai/chat/route.ts` around lines 752 - 755, The saved assistant message
is replacing earlier streamed content because assistantText is overwritten with
followUpText.trim(); instead, append the follow-up to preserve the initial
content: update the logic around assistantText and followUpText so that if
followUpText.trim() is non-empty you set assistantText = (assistantText ||
"").trimEnd() + "\n\n" + followUpText.trim() (or similar spacing), ensuring you
preserve any existing assistantText and only add the follow-up; adjust any
trimming to avoid duplicating whitespace.

…ow-up failure

- buildContents() in gemini.ts puts system prompt as user role, so when
  follow-up call sends history=[] + user message, two consecutive user
  turns are created which Gemini API silently rejects
- Fixed by inserting model acknowledgement turn between system prompt
  and first user message when needed
- Added visible error fallback in route.ts follow-up catch block
- Added monitor.error() call so failures appear in session logs
@github-actions github-actions bot added the documentation Improvements or additions to documentation label Mar 9, 2026
- vectorstore.ts: Fixed Prisma ::vector(768) cast using Prisma.raw()
- embeddings.ts: Switched from deprecated Together.ai GTE-ModernBERT to Gemini gemini-embedding-001 (768-dim, free)
- executor.ts: Added filepath param for viewport screenshot + file-read fallback + nested error detection
- agents.ts: Fixed isMcpSuccess() to check nested result.error BEFORE trusting top-level status
- screenshot.ts: Added filepath param + file-based fallback read
- route.ts: Fixed LogNamespace type error (followup -> system)
- Successfully ingested 135 blender-scripts documents into pgvector
Copy link

@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)
lib/ai/vectorstore.ts (1)

66-89: ⚠️ Potential issue | 🟠 Major

Make addDocuments() atomic.

A failure between Line 75 and Line 86 commits a prefix of the batch and then throws, so a retry can duplicate already-inserted rows. Wrap the loop in prisma.$transaction(...) or replace it with one multi-row insert.

🔁 Proposed fix
 export async function addDocuments(docs: Document[]): Promise<string[]> {
   const embeddings = await embedTexts(docs.map((d) => d.content))
-  const ids: string[] = []
-
-  for (let i = 0; i < docs.length; i++) {
-    const doc = docs[i]
-    const embedding = embeddings[i].embedding
-
-    const vec = vectorLiteral(embedding)
-    const result = await prisma.$queryRaw<{ id: string }[]>`
+  return prisma.$transaction(async (tx) => {
+    const ids: string[] = []
+
+    for (let i = 0; i < docs.length; i++) {
+      const doc = docs[i]
+      const embedding = embeddings[i].embedding
+      const vec = vectorLiteral(embedding)
+      const result = await tx.$queryRaw<{ id: string }[]>`
       INSERT INTO document_embeddings (id, content, embedding, metadata, source, "createdAt")
       VALUES (
         gen_random_uuid(),
         ${doc.content},
         ${vec},
         ${JSON.stringify(doc.metadata ?? {})}::jsonb,
         ${doc.source ?? null},
         NOW()
       )
       RETURNING id::text
     `
 
-    ids.push(result[0].id)
-  }
-
-  return ids
+      ids.push(result[0].id)
+    }
+
+    return ids
+  })
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ai/vectorstore.ts` around lines 66 - 89, The addDocuments function
currently inserts rows one-by-one using prisma.$queryRaw inside a loop (see
addDocuments, prisma.$queryRaw, vectorLiteral), which can leave a
partially-committed batch on error; make the operation atomic by wrapping the
loop in a single prisma.$transaction that executes all inserts as one
transaction or by building and executing a single multi-row INSERT for all docs
(ensuring embeddings and vectorLiteral values are prepared beforehand), then
return the collected IDs from the transaction result; update error handling to
propagate/rollback as needed.
♻️ Duplicate comments (1)
app/api/ai/chat/route.ts (1)

766-768: ⚠️ Potential issue | 🟡 Minor

Inconsistent handling between success and error paths.

On success (line 757), assistantText is replaced with the follow-up. On error (lines 766-768), the fallback is appended to the existing assistantText. This inconsistency means:

  • Successful follow-up → DB stores only follow-up text
  • Failed follow-up → DB stores initial response + fallback

Consider making both paths consistent (either always replace or always append).

Suggested fix — consistent append behavior
             // Replace assistant text with follow-up so it's saved to DB
             if (followUpText.trim()) {
-              assistantText = followUpText.trim()
+              assistantText += "\n\n" + followUpText.trim()
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/ai/chat/route.ts` around lines 766 - 768, The success and error paths
handle the follow-up inconsistently: in the success branch the code replaces
assistantText with the follow-up, while in the error branch it appends the
fallback to assistantText; make them consistent by changing the success path to
append the follow-up instead of replacing it (update the code around the send({
type: "followup_delta", delta: ... }) success handling so it does assistantText
+= `\n\n${followupText}` rather than assistantText = `\n\n${followupText}`),
ensuring both send calls and the stored assistantText contain appended
follow-up/fallback text.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@debug_error.json`:
- Around line 1-5: The file debug_error.json contains a stale embedding error
referencing models/text-embedding-004 which conflicts with the current model
used in lib/ai/embeddings.ts (gemini-embedding-001); either delete this debug
artifact, move it to a debug/ directory or .gitignore it, or update its
contents/messages to accurately reflect the current model name and call stack
(e.g., reference embedTexts and addDocuments locations) so future developers
aren’t misled by an outdated model name; perform the chosen action and ensure no
other debug JSON files with outdated model names remain in the repo.

In `@GEMINI.md`:
- Around line 1-55: Add missing blank lines around top-level and secondary
headings and the markdown table to satisfy markdownlint: ensure there is an
empty line before and after each heading such as "ModelForge – Active
Development Notes", "## Current Task", "## Progress Summary", "### Unified
Session Monitor (`lib/monitoring/logger.ts`)" etc., and add a blank line above
the table header and below the table block so the pipe-table is isolated; update
GEMINI.md accordingly to remove the linter warnings.

In `@ingestion_log.txt`:
- Line 13: The ingestion failed because docstring-style metadata (fields like
"Category:", "Blender:", "Source:") in the listed files (e.g.,
blender_api_pitfalls.py, displacement_textures.py, hdri_lighting.py,
import_neural_mesh.py, interior_rooms.py, photorealistic_materials.py,
professional_materials.py, render_settings.py, volumetric_effects.py) is not
valid JSON for the metadata parser; fix by either (A) converting the top-of-file
docstrings in each of those Python files into a valid JSON metadata block
(ensuring keys/values are proper JSON and the parser can detect it), or (B)
update the metadata parser (the ingestion function that reads file metadata) to
detect and parse plain-text docstring metadata: implement a docstring parser
that recognizes lines like "Category: <value>", "Blender: <value>", "Source:
<value>", maps them to the metadata schema, and falls back to existing JSON
parsing; apply the chosen fix consistently and re-run the ingestion so titles
are populated instead of "(Untitled)".

In `@lib/ai/vectorstore.ts`:
- Around line 36-38: The vectorLiteral function must validate the embedding
before casting: check that the embedding array length equals the expected
dimension (e.g. EMBEDDING_DIM or 768) and that every element is a finite number
(no NaN/Infinity); if validation fails, throw a clear application error instead
of constructing the SQL. Modify vectorLiteral to perform these checks on the
embedding parameter and only return Prisma.sql`${...}${VECTOR_CAST}` when
validation passes, referencing the existing VECTOR_CAST symbol and an
EMBEDDING_DIM constant (or literal 768) to locate where to enforce the expected
shape.

In `@lib/mcp/screenshot.ts`:
- Line 26: The temp file created as filepath/screenshotPath can leak if an error
occurs before the unlink because deletion is only inside the try; update the
function that writes/reads the screenshot so the unlink is performed in a
finally block (or equivalent cleanup path) to guarantee removal regardless of
success or failure: keep the readFile/readStream logic in the try, but move
fs.unlink/fs.unlinkSync for filepath into a finally block and guard it (check
existence / swallow ENOENT) so the temp file is always attempted to be removed
even on early exceptions.

In `@lib/orchestration/executor.ts`:
- Around line 304-315: The file-read + cleanup logic in executor.ts duplicates
the same pattern in screenshot.ts; extract that block into a shared async
utility (e.g., readAndCleanupTempFile(filepath)) that imports fs/promises, reads
the file, returns its base64 string (or null on failure), and always attempts to
unlink the temp file swallowing unlink errors; then replace the duplicated code
in the executor (the imageBase64 fallback) and the corresponding block in
screenshot.ts to call readAndCleanupTempFile(screenshotPath) so both sites share
consistent read+cleanup behavior.

---

Outside diff comments:
In `@lib/ai/vectorstore.ts`:
- Around line 66-89: The addDocuments function currently inserts rows one-by-one
using prisma.$queryRaw inside a loop (see addDocuments, prisma.$queryRaw,
vectorLiteral), which can leave a partially-committed batch on error; make the
operation atomic by wrapping the loop in a single prisma.$transaction that
executes all inserts as one transaction or by building and executing a single
multi-row INSERT for all docs (ensuring embeddings and vectorLiteral values are
prepared beforehand), then return the collected IDs from the transaction result;
update error handling to propagate/rollback as needed.

---

Duplicate comments:
In `@app/api/ai/chat/route.ts`:
- Around line 766-768: The success and error paths handle the follow-up
inconsistently: in the success branch the code replaces assistantText with the
follow-up, while in the error branch it appends the fallback to assistantText;
make them consistent by changing the success path to append the follow-up
instead of replacing it (update the code around the send({ type:
"followup_delta", delta: ... }) success handling so it does assistantText +=
`\n\n${followupText}` rather than assistantText = `\n\n${followupText}`),
ensuring both send calls and the stored assistantText contain appended
follow-up/fallback text.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 42344d72-a349-4ec8-9ffe-52102c5fa835

📥 Commits

Reviewing files that changed from the base of the PR and between b086cfe and 46cf23f.

📒 Files selected for processing (10)
  • GEMINI.md
  • app/api/ai/chat/route.ts
  • debug_error.json
  • ingestion_log.txt
  • lib/ai/agents.ts
  • lib/ai/embeddings.ts
  • lib/ai/vectorstore.ts
  • lib/gemini.ts
  • lib/mcp/screenshot.ts
  • lib/orchestration/executor.ts

Comment on lines 1 to 5
{
"message": "400 Unable to access non-serverless model togethercomputer/m2-bert-80M-32k-retrieval. Please visit https://api.together.ai/models/togethercomputer/m2-bert-80M-32k-retrieval to create and start a new dedicated endpoint for the model.",
"code": "model_not_available",
"stack": "Error: 400 Unable to access non-serverless model togethercomputer/m2-bert-80M-32k-retrieval. Please visit https://api.together.ai/models/togethercomputer/m2-bert-80M-32k-retrieval to create and start a new dedicated endpoint for the model.\n at Function.generate (/media/krist/CrucialX9/cursor-projects/projects/project02/BlenderAI/node_modules/openai/src/error.ts:72:14)\n at OpenAI.makeStatusError (/media/krist/CrucialX9/cursor-projects/projects/project02/BlenderAI/node_modules/openai/src/core.ts:462:21)\n at OpenAI.makeRequest (/media/krist/CrucialX9/cursor-projects/projects/project02/BlenderAI/node_modules/openai/src/core.ts:526:24)\n at process.processTicksAndRejections (node:internal/process/task_queues:105:5)\n at async embedTexts (/media/krist/CrucialX9/cursor-projects/projects/project02/BlenderAI/lib/ai/embeddings.ts:51:26)\n at async addDocuments (/media/krist/CrucialX9/cursor-projects/projects/project02/BlenderAI/lib/ai/vectorstore.ts:51:22)\n at async ingest (/media/krist/CrucialX9/cursor-projects/projects/project02/BlenderAI/scripts/ingest-blender-docs.ts:114:25)",
"error": {
"status": 400,
"headers": {
"access-control-allow-origin": "*",
"alt-svc": "h3=\":443\"; ma=86400",
"cf-cache-status": "DYNAMIC",
"cf-ray": "9cbca10fc8337122-TLL",
"connection": "keep-alive",
"content-length": "400",
"content-type": "application/json; charset=utf-8",
"date": "Tue, 10 Feb 2026 15:27:13 GMT",
"etag": "W/\"190-oVQ8W47NePIU7/SEdtJPLNSlchs\"",
"retry-after": "2",
"server": "cloudflare",
"strict-transport-security": "max-age=15552000; includeSubDomains",
"vary": "Accept-Encoding",
"x-amzn-trace-id": "7d8f5a65-17d2-4eb8-a397-02a6e560a3e9-noamzn",
"x-api-received": "2026-02-10T15:27:12.131Z",
"x-ratelimit": "false",
"x-ratelimit-limit": "50",
"x-ratelimit-limit-tokens": "0",
"x-ratelimit-remaining": "99",
"x-ratelimit-remaining-tokens": "0",
"x-ratelimit-reset": "2",
"x-request-id": "oWpzAYx-4msxKE-9cbca10fc8337122"
},
"request_id": "oWpzAYx-4msxKE-9cbca10fc8337122",
"error": {
"message": "Unable to access non-serverless model togethercomputer/m2-bert-80M-32k-retrieval. Please visit https://api.together.ai/models/togethercomputer/m2-bert-80M-32k-retrieval to create and start a new dedicated endpoint for the model.",
"type": "invalid_request_error",
"param": null,
"code": "model_not_available"
},
"code": "model_not_available",
"param": null,
"type": "invalid_request_error"
}
"message": "Gemini Embedding API error 404: {\n \"error\": {\n \"code\": 404,\n \"message\": \"models/text-embedding-004 is not found for API version v1beta, or is not supported for embedContent. Call ListModels to see the list of available models and their supported methods.\",\n \"status\": \"NOT_FOUND\"\n }\n}\n",
"stack": "Error: Gemini Embedding API error 404: {\n \"error\": {\n \"code\": 404,\n \"message\": \"models/text-embedding-004 is not found for API version v1beta, or is not supported for embedContent. Call ListModels to see the list of available models and their supported methods.\",\n \"status\": \"NOT_FOUND\"\n }\n}\n\n at embedTexts (C:\\Users\\krist\\Desktop\\Cursor-Projects\\Projects\\modelforge\\ModelForge\\lib\\ai\\embeddings.ts:61:19)\n at process.processTicksAndRejections (node:internal/process/task_queues:105:5)\n at async addDocuments (C:\\Users\\krist\\Desktop\\Cursor-Projects\\Projects\\modelforge\\ModelForge\\lib\\ai\\vectorstore.ts:67:22)\n at async ingest (C:\\Users\\krist\\Desktop\\Cursor-Projects\\Projects\\modelforge\\ModelForge\\scripts\\ingest-blender-docs.ts:114:25)",
"error": {}
} No newline at end of file
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider removing or updating this debug artifact.

This error log references models/text-embedding-004, but the current lib/ai/embeddings.ts uses gemini-embedding-001. This stale debug file could confuse future developers investigating embedding issues. Either remove it, update it to reflect the current model, or move it to a dedicated debug/ or .gitignore-excluded location.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@debug_error.json` around lines 1 - 5, The file debug_error.json contains a
stale embedding error referencing models/text-embedding-004 which conflicts with
the current model used in lib/ai/embeddings.ts (gemini-embedding-001); either
delete this debug artifact, move it to a debug/ directory or .gitignore it, or
update its contents/messages to accurately reflect the current model name and
call stack (e.g., reference embedTexts and addDocuments locations) so future
developers aren’t misled by an outdated model name; perform the chosen action
and ensure no other debug JSON files with outdated model names remain in the
repo.

Comment on lines +1 to +55
# ModelForge – Active Development Notes

## Current Task
**Studio Pipeline Hardening** – Fixing Blender agent bugs and improving post-execution UX.

## Progress Summary
- Gemini API switched to `gemini-3.1-pro-preview` model with `v1beta` endpoint
- Studio `workflowMode` fixed: `autopilot` (not `studio`) for actual execution
- Monitoring system (MonitoringPanel, StepSessionDrawer) fully wired
- Session drawer now shows plan execution summary, command results, and pipeline monitor
- Added `StepPlanData` + `StepCommandResult` types for structured execution data
- Updated `complete` event handler to extract planning metadata and command suggestions
- **Fixed**: `get_viewport_screenshot` → `max_size` param instead of invalid `width`/`height`
- **Fixed**: Defensive param normalization in executor for LLM-hallucinated width/height
- **Fixed**: Gemini API `buildContents()` consecutive user messages bug (caused follow-up to silently fail)
- **Fixed**: Post-execution follow-up with visible error fallback in catch block
- **Fixed**: CRAG pipeline — `vectorstore.ts` used `::vector(768)` in Prisma tagged templates which PostgreSQL rejected. Switched to `Prisma.raw()` for type casts.
- **Fixed**: Embedding model — switched from Together.ai `GTE-ModernBERT-base` (deprecated) to Gemini `gemini-embedding-001` (768-dim, free). No DB migration needed.
- **Fixed**: Viewport screenshot — Blender MCP server requires `filepath` param. Added file-based fallback (read from disk if no inline base64).
- **Fixed**: `isMcpSuccess()` — now checks nested `result.error` BEFORE trusting top-level `status: "success"`.
- **Ingested**: 135 blender-scripts documents into pgvector vectorstore across 39 categories.

## Monitoring & Logging Reference
**IMPORTANT**: Always check these files when debugging the Blender agent pipeline.

### Unified Session Monitor (`lib/monitoring/logger.ts`)
- **Class**: `MonitoringSession` (created via `createMonitoringSession(sessionId)`)
- **Console output**: Colorized, structured logs in the Next.js dev server terminal
- **File output**: `logs/pipeline.log` — NDJSON, one JSON entry per log call
- **Session summary**: `logs/session-{first8chars-of-id}.json` — full session JSON (timers, counts, costs)
- **Namespaces**: planner, executor, crag, neural, vision, rag, strategy, mcp, workflow, system
- **Features**: Timers (`startTimer`/`endTimer`), neural cost tracking, RAG stats, log callbacks

### Execution Monitor (`lib/orchestration/monitor.ts`)
- **Function**: `recordExecutionLog(record)` — appends to `logs/orchestration.ndjson`
- **Content**: Full execution records with plan summary, command results, scene summary

### Log File Locations
| File | Content |
|------|---------|
| `logs/pipeline.log` | Per-entry NDJSON from MonitoringSession |
| `logs/session-{id}.json` | Full session summary (timers, costs, stats) |
| `logs/orchestration.ndjson` | Execution records (plan + command results) |
| `logs/latest-run.json` | Last execution record (legacy) |

### Visual Feedback Flow
1. `route.ts:626` sets `enableVisualFeedback: true`
2. `executor.ts:265-380` captures viewport screenshot via MCP (`get_viewport_screenshot`)
3. `executor.ts:315` calls `suggestImprovements(imageBase64, userRequest)` — Gemini Vision analysis
4. If high-priority issues found → generates correction code → executes fix → re-captures
5. Max 2 visual iterations by default (`maxVisualIterations ?? 2`)

## Next Steps
- End-to-end test: verify follow-up text appears in UI after Blender agent execution
- Review CodeRabbit feedback on PR
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Fix markdown formatting per linter warnings.

Static analysis (markdownlint) flags missing blank lines around headings and tables. While non-blocking, fixing these improves consistency with standard markdown style.

Add blank lines around headings and table
 # ModelForge – Active Development Notes
 
 ## Current Task
+
 **Studio Pipeline Hardening** – Fixing Blender agent bugs and improving post-execution UX.
 
 ## Progress Summary
+
 - Gemini API switched to `gemini-3.1-pro-preview` model with `v1beta` endpoint
 ...
 
 ## Monitoring & Logging Reference
+
 **IMPORTANT**: Always check these files when debugging the Blender agent pipeline.
 
 ### Unified Session Monitor (`lib/monitoring/logger.ts`)
+
 - **Class**: `MonitoringSession` (created via `createMonitoringSession(sessionId)`)
 ...
 
 ### Execution Monitor (`lib/orchestration/monitor.ts`)
+
 - **Function**: `recordExecutionLog(record)` — appends to `logs/orchestration.ndjson`
 ...
 
 ### Log File Locations
+
 | File | Content |
 |------|---------|
 ...
+
 ### Visual Feedback Flow
+
 1. `route.ts:626` sets `enableVisualFeedback: true`
 ...
 
 ## Next Steps
+
 - End-to-end test: verify follow-up text appears in UI after Blender agent execution
📝 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
# ModelForge – Active Development Notes
## Current Task
**Studio Pipeline Hardening** – Fixing Blender agent bugs and improving post-execution UX.
## Progress Summary
- Gemini API switched to `gemini-3.1-pro-preview` model with `v1beta` endpoint
- Studio `workflowMode` fixed: `autopilot` (not `studio`) for actual execution
- Monitoring system (MonitoringPanel, StepSessionDrawer) fully wired
- Session drawer now shows plan execution summary, command results, and pipeline monitor
- Added `StepPlanData` + `StepCommandResult` types for structured execution data
- Updated `complete` event handler to extract planning metadata and command suggestions
- **Fixed**: `get_viewport_screenshot``max_size` param instead of invalid `width`/`height`
- **Fixed**: Defensive param normalization in executor for LLM-hallucinated width/height
- **Fixed**: Gemini API `buildContents()` consecutive user messages bug (caused follow-up to silently fail)
- **Fixed**: Post-execution follow-up with visible error fallback in catch block
- **Fixed**: CRAG pipeline — `vectorstore.ts` used `::vector(768)` in Prisma tagged templates which PostgreSQL rejected. Switched to `Prisma.raw()` for type casts.
- **Fixed**: Embedding model — switched from Together.ai `GTE-ModernBERT-base` (deprecated) to Gemini `gemini-embedding-001` (768-dim, free). No DB migration needed.
- **Fixed**: Viewport screenshot — Blender MCP server requires `filepath` param. Added file-based fallback (read from disk if no inline base64).
- **Fixed**: `isMcpSuccess()` — now checks nested `result.error` BEFORE trusting top-level `status: "success"`.
- **Ingested**: 135 blender-scripts documents into pgvector vectorstore across 39 categories.
## Monitoring & Logging Reference
**IMPORTANT**: Always check these files when debugging the Blender agent pipeline.
### Unified Session Monitor (`lib/monitoring/logger.ts`)
- **Class**: `MonitoringSession` (created via `createMonitoringSession(sessionId)`)
- **Console output**: Colorized, structured logs in the Next.js dev server terminal
- **File output**: `logs/pipeline.log` — NDJSON, one JSON entry per log call
- **Session summary**: `logs/session-{first8chars-of-id}.json` — full session JSON (timers, counts, costs)
- **Namespaces**: planner, executor, crag, neural, vision, rag, strategy, mcp, workflow, system
- **Features**: Timers (`startTimer`/`endTimer`), neural cost tracking, RAG stats, log callbacks
### Execution Monitor (`lib/orchestration/monitor.ts`)
- **Function**: `recordExecutionLog(record)` — appends to `logs/orchestration.ndjson`
- **Content**: Full execution records with plan summary, command results, scene summary
### Log File Locations
| File | Content |
|------|---------|
| `logs/pipeline.log` | Per-entry NDJSON from MonitoringSession |
| `logs/session-{id}.json` | Full session summary (timers, costs, stats) |
| `logs/orchestration.ndjson` | Execution records (plan + command results) |
| `logs/latest-run.json` | Last execution record (legacy) |
### Visual Feedback Flow
1. `route.ts:626` sets `enableVisualFeedback: true`
2. `executor.ts:265-380` captures viewport screenshot via MCP (`get_viewport_screenshot`)
3. `executor.ts:315` calls `suggestImprovements(imageBase64, userRequest)` — Gemini Vision analysis
4. If high-priority issues found → generates correction code → executes fix → re-captures
5. Max 2 visual iterations by default (`maxVisualIterations ?? 2`)
## Next Steps
- End-to-end test: verify follow-up text appears in UI after Blender agent execution
- Review CodeRabbit feedback on PR
# ModelForge – Active Development Notes
## Current Task
**Studio Pipeline Hardening** – Fixing Blender agent bugs and improving post-execution UX.
## Progress Summary
- Gemini API switched to `gemini-3.1-pro-preview` model with `v1beta` endpoint
- Studio `workflowMode` fixed: `autopilot` (not `studio`) for actual execution
- Monitoring system (MonitoringPanel, StepSessionDrawer) fully wired
- Session drawer now shows plan execution summary, command results, and pipeline monitor
- Added `StepPlanData` + `StepCommandResult` types for structured execution data
- Updated `complete` event handler to extract planning metadata and command suggestions
- **Fixed**: `get_viewport_screenshot``max_size` param instead of invalid `width`/`height`
- **Fixed**: Defensive param normalization in executor for LLM-hallucinated width/height
- **Fixed**: Gemini API `buildContents()` consecutive user messages bug (caused follow-up to silently fail)
- **Fixed**: Post-execution follow-up with visible error fallback in catch block
- **Fixed**: CRAG pipeline — `vectorstore.ts` used `::vector(768)` in Prisma tagged templates which PostgreSQL rejected. Switched to `Prisma.raw()` for type casts.
- **Fixed**: Embedding model — switched from Together.ai `GTE-ModernBERT-base` (deprecated) to Gemini `gemini-embedding-001` (768-dim, free). No DB migration needed.
- **Fixed**: Viewport screenshot — Blender MCP server requires `filepath` param. Added file-based fallback (read from disk if no inline base64).
- **Fixed**: `isMcpSuccess()` — now checks nested `result.error` BEFORE trusting top-level `status: "success"`.
- **Ingested**: 135 blender-scripts documents into pgvector vectorstore across 39 categories.
## Monitoring & Logging Reference
**IMPORTANT**: Always check these files when debugging the Blender agent pipeline.
### Unified Session Monitor (`lib/monitoring/logger.ts`)
- **Class**: `MonitoringSession` (created via `createMonitoringSession(sessionId)`)
- **Console output**: Colorized, structured logs in the Next.js dev server terminal
- **File output**: `logs/pipeline.log` — NDJSON, one JSON entry per log call
- **Session summary**: `logs/session-{first8chars-of-id}.json` — full session JSON (timers, counts, costs)
- **Namespaces**: planner, executor, crag, neural, vision, rag, strategy, mcp, workflow, system
- **Features**: Timers (`startTimer`/`endTimer`), neural cost tracking, RAG stats, log callbacks
### Execution Monitor (`lib/orchestration/monitor.ts`)
- **Function**: `recordExecutionLog(record)` — appends to `logs/orchestration.ndjson`
- **Content**: Full execution records with plan summary, command results, scene summary
### Log File Locations
| File | Content |
|------|---------|
| `logs/pipeline.log` | Per-entry NDJSON from MonitoringSession |
| `logs/session-{id}.json` | Full session summary (timers, costs, stats) |
| `logs/orchestration.ndjson` | Execution records (plan + command results) |
| `logs/latest-run.json` | Last execution record (legacy) |
### Visual Feedback Flow
1. `route.ts:626` sets `enableVisualFeedback: true`
2. `executor.ts:265-380` captures viewport screenshot via MCP (`get_viewport_screenshot`)
3. `executor.ts:315` calls `suggestImprovements(imageBase64, userRequest)` — Gemini Vision analysis
4. If high-priority issues found → generates correction code → executes fix → re-captures
5. Max 2 visual iterations by default (`maxVisualIterations ?? 2`)
## Next Steps
- End-to-end test: verify follow-up text appears in UI after Blender agent execution
- Review CodeRabbit feedback on PR
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 3-3: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 6-6: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 23-23: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 26-26: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 34-34: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 38-38: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 39-39: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 46-46: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 53-53: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@GEMINI.md` around lines 1 - 55, Add missing blank lines around top-level and
secondary headings and the markdown table to satisfy markdownlint: ensure there
is an empty line before and after each heading such as "ModelForge – Active
Development Notes", "## Current Task", "## Progress Summary", "### Unified
Session Monitor (`lib/monitoring/logger.ts`)" etc., and add a blank line above
the table header and below the table block so the pipe-table is isolated; update
GEMINI.md accordingly to remove the linter warnings.

­ƒôä Prepared: auto_rigify.py (Automated Rigify Rigging Pipeline) [utility]
­ƒôä Prepared: auto_uv_unwrap.py (Automated UV Unwrap Pipeline) [utility]
­ƒôä Prepared: batch_utils.py (Batch Operations Utilities) [utility]
ÔÜá´©Å Could not parse metadata in blender_api_pitfalls.py: Unexpected token 'B', "Blender Py"... is not valid JSON
Copy link

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
# Description: Inspect metadata headers in files that failed JSON parsing

files=(
  "blender_api_pitfalls.py"
  "displacement_textures.py"
  "hdri_lighting.py"
  "import_neural_mesh.py"
  "interior_rooms.py"
  "photorealistic_materials.py"
  "professional_materials.py"
  "render_settings.py"
  "volumetric_effects.py"
)

for file in "${files[@]}"; do
  echo "=== $file ==="
  fd -t f "$file" data/blender-scripts --exec head -n 20 {}
  echo ""
done

Repository: Ker102/ModelForge

Length of output: 7439


Address metadata format mismatch in 9 ingested files.

The ingestion process encountered JSON parse errors for metadata in these 9 files, all falling back to "(Untitled)":

  • blender_api_pitfalls.py
  • displacement_textures.py
  • hdri_lighting.py
  • import_neural_mesh.py
  • interior_rooms.py
  • photorealistic_materials.py
  • professional_materials.py
  • render_settings.py
  • volumetric_effects.py

All 9 files contain Python docstrings with plain-text structured metadata (fields like "Category:", "Blender:", "Source:"), but the metadata parser expects JSON format. This format mismatch causes parse failures and loss of descriptive titles, which may impact searchability and RAG retrieval quality.

Either convert these docstrings to JSON metadata or update the parser to handle docstring-based metadata. Given the retrieved learning about re-ingesting Blender scripts after RAG library updates, address this inconsistency in the next ingestion cycle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ingestion_log.txt` at line 13, The ingestion failed because docstring-style
metadata (fields like "Category:", "Blender:", "Source:") in the listed files
(e.g., blender_api_pitfalls.py, displacement_textures.py, hdri_lighting.py,
import_neural_mesh.py, interior_rooms.py, photorealistic_materials.py,
professional_materials.py, render_settings.py, volumetric_effects.py) is not
valid JSON for the metadata parser; fix by either (A) converting the top-of-file
docstrings in each of those Python files into a valid JSON metadata block
(ensuring keys/values are proper JSON and the parser can detect it), or (B)
update the metadata parser (the ingestion function that reads file metadata) to
detect and parse plain-text docstring metadata: implement a docstring parser
that recognizes lines like "Category: <value>", "Blender: <value>", "Source:
<value>", maps them to the metadata schema, and falls back to existing JSON
parsing; apply the chosen fix consistently and re-run the ingestion so titles
are populated instead of "(Untitled)".

Comment on lines +36 to +38
function vectorLiteral(embedding: number[]) {
return Prisma.sql`${`[${embedding.join(",")}]`}${VECTOR_CAST}`
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate embedding shape before handing it to Postgres.

A non-768 vector or non-finite value currently turns into a pgvector cast/operator failure. Fail fast here so writes and searches surface a deterministic application error instead.

🛡️ Proposed fix
-function vectorLiteral(embedding: number[]) {
-  return Prisma.sql`${`[${embedding.join(",")}]`}${VECTOR_CAST}`
+function vectorLiteral(embedding: number[]): Prisma.Sql {
+  if (embedding.length !== EMBEDDING_DIMENSIONS) {
+    throw new Error(
+      `Expected ${EMBEDDING_DIMENSIONS} embedding values, got ${embedding.length}`
+    )
+  }
+  if (embedding.some((value) => !Number.isFinite(value))) {
+    throw new Error("Embedding contains a non-finite value")
+  }
+  return Prisma.sql`${`[${embedding.join(",")}]`}${VECTOR_CAST}`
 }
📝 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 vectorLiteral(embedding: number[]) {
return Prisma.sql`${`[${embedding.join(",")}]`}${VECTOR_CAST}`
}
function vectorLiteral(embedding: number[]): Prisma.Sql {
if (embedding.length !== EMBEDDING_DIMENSIONS) {
throw new Error(
`Expected ${EMBEDDING_DIMENSIONS} embedding values, got ${embedding.length}`
)
}
if (embedding.some((value) => !Number.isFinite(value))) {
throw new Error("Embedding contains a non-finite value")
}
return Prisma.sql`${`[${embedding.join(",")}]`}${VECTOR_CAST}`
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ai/vectorstore.ts` around lines 36 - 38, The vectorLiteral function must
validate the embedding before casting: check that the embedding array length
equals the expected dimension (e.g. EMBEDDING_DIM or 768) and that every element
is a finite number (no NaN/Infinity); if validation fails, throw a clear
application error instead of constructing the SQL. Modify vectorLiteral to
perform these checks on the embedding parameter and only return
Prisma.sql`${...}${VECTOR_CAST}` when validation passes, referencing the
existing VECTOR_CAST symbol and an EMBEDDING_DIM constant (or literal 768) to
locate where to enforce the expected shape.

format?: "png" | "jpeg"
} = {}): Promise<ViewportScreenshotResponse> {
const client = createMcpClient()
const filepath = path.join(tmpdir(), `modelforge_viewport_${Date.now()}.png`)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Temp file cleanup not guaranteed on early errors.

The temp file created at screenshotPath is only deleted inside the try block (line 57). If an error is thrown after the MCP server writes the file but before the read/unlink (e.g., at line 45), the temp file will leak. While OS-level temp cleanup mitigates this, consider adding cleanup in a finally block for robustness.

Suggested fix — cleanup in finally block
 export async function getViewportScreenshot(options: {
     maxSize?: number
     format?: "png" | "jpeg"
 } = {}): Promise<ViewportScreenshotResponse> {
     const client = createMcpClient()
     const filepath = path.join(tmpdir(), `modelforge_viewport_${Date.now()}.png`)
+    let needsCleanup = false
 
     try {
         const response = await client.execute<ViewportScreenshotResponse>({
             ...
         })
         ...
+        needsCleanup = true  // MCP server likely wrote the file
+        
         // If not inline, read from the saved filepath
         if (!imageBase64) {
             try {
                 const buffer = await readFile(filepath)
                 imageBase64 = buffer.toString("base64")
-                // Clean up temp file
-                await unlink(filepath).catch(() => { })
+                needsCleanup = false  // Will cleanup in finally
             } catch {
                 throw new Error("Screenshot file not found - Blender MCP may not have saved it")
             }
         }
         ...
     } finally {
+        if (needsCleanup) {
+            await unlink(filepath).catch(() => { })
+        }
         await client.close()
     }
 }

Also applies to: 51-61

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mcp/screenshot.ts` at line 26, The temp file created as
filepath/screenshotPath can leak if an error occurs before the unlink because
deletion is only inside the try; update the function that writes/reads the
screenshot so the unlink is performed in a finally block (or equivalent cleanup
path) to guarantee removal regardless of success or failure: keep the
readFile/readStream logic in the try, but move fs.unlink/fs.unlinkSync for
filepath into a finally block and guard it (check existence / swallow ENOENT) so
the temp file is always attempted to be removed even on early exceptions.

Comment on lines +304 to +315
// If no inline image, try reading from the saved filepath
if (!imageBase64) {
try {
const { readFile, unlink } = await import("fs/promises")
const buffer = await readFile(screenshotPath)
imageBase64 = buffer.toString("base64")
// Clean up temp file
await unlink(screenshotPath).catch(() => { })
} catch {
// File not found — screenshot wasn't saved
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

File-reading fallback duplicates logic from lib/mcp/screenshot.ts.

The file read + cleanup pattern here mirrors lines 51-61 in screenshot.ts. Consider extracting to a shared utility (e.g., readAndCleanupTempFile(filepath)) to reduce duplication and ensure consistent cleanup behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/orchestration/executor.ts` around lines 304 - 315, The file-read +
cleanup logic in executor.ts duplicates the same pattern in screenshot.ts;
extract that block into a shared async utility (e.g.,
readAndCleanupTempFile(filepath)) that imports fs/promises, reads the file,
returns its base64 string (or null on failure), and always attempts to unlink
the temp file swallowing unlink errors; then replace the duplicated code in the
executor (the imageBase64 fallback) and the corresponding block in screenshot.ts
to call readAndCleanupTempFile(screenshotPath) so both sites share consistent
read+cleanup behavior.

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

Labels

backend configuration desktop documentation Improvements or additions to documentation frontend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant