Conversation
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
📝 WalkthroughWalkthroughThis pull request migrates RenPy script parsing from the backend to the frontend. The backend removes its RenPyParser implementation, related tests, and parsing logic, returning empty trees from endpoints while saving scripts directly. The frontend introduces a new RenPyParser implementation with corresponding tests and golden fixtures, integrating local parsing into API handlers. Changes
Sequence DiagramsequenceDiagram
participant User
participant Frontend
participant RenPyParser as Frontend<br/>RenPyParser
participant Backend
participant Database
Note over Frontend,Database: Old Flow (Backend Parsing)
User->>Frontend: Upload script
Frontend->>Backend: POST /scripts/parse (content)
activate Backend
Backend->>Backend: Parse RenPyParser.parse_text()
Backend->>Database: Save script + tree
Backend-->>Frontend: Return script_id, tree
deactivate Backend
Frontend-->>User: Display tree
Note over Frontend,Database: New Flow (Frontend Parsing)
User->>Frontend: Upload script
Frontend->>RenPyParser: parse(content)
activate RenPyParser
RenPyParser-->>Frontend: Return parsed tree
deactivate RenPyParser
Frontend->>Backend: POST /scripts/parse (content)
activate Backend
Backend->>Database: Save script (no parsing)
Backend-->>Frontend: Return script_id, empty_tree
deactivate Backend
Frontend->>Frontend: Merge local tree + backend metadata
Frontend-->>User: Display tree
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 380dd1f5af
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # Broadcast update | ||
| # Note: We no longer broadcast the full parsed tree because parsing is moved to frontend. | ||
| # Ideally, we should broadcast a "content_updated" event and let clients re-fetch or re-parse. | ||
| # For now, we omit the structure update broadcast. |
There was a problem hiding this comment.
Emit a structure/content update after edits
This change removes the only server-side trigger that tells other clients to reload the script graph after edits. The frontend only refreshes its parsed tree when it receives an updateStructure WebSocket message (see CollabContext’s updateStructure case and the reload hook in EditorPage), while node_updated is just logged. By omitting any broadcast here, collaborators editing the same script will never get a reload signal, so their graphs stay stale until a manual refresh. If parsing is now frontend-only, the backend still needs to emit an explicit “content_updated”/“updateStructure” event so other clients re-fetch and re-parse.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
frontend/src/services/api.ts (1)
62-97:⚠️ Potential issue | 🟠 MajorLocal parsing failure prevents the file from being saved.
If
parser.parse(text)at Line 67 throws (e.g., for an unexpected script construct), the function exits before the upload at Line 83, so the user's file is never persisted to the backend. Consider uploading first (to guarantee persistence), then parsing locally — or wrapping the parse in a try/catch and returning a fallback empty tree on failure.🛡️ Suggested reordering: save first, then parse
export const parseScript = async (file: File, projectId?: string): Promise<ParsedScriptResponse> => { - // 1. Read file locally - const text = await file.text(); - - // 2. Parse locally to validate and get the tree - const parser = new RenPyParser(); - const tree = parser.parse(text); - console.log('[Frontend Parser] Parsed local file:', file.name); - - // 3. Upload to backend (to save) const formData = new FormData(); formData.append('file', file); if (projectId) { formData.append('project_id', projectId); } + const targetUrl = apiClient.defaults.baseURL + '/scripts/parse'; + try { const response = await apiClient.post<ParsedScriptResponse>('/scripts/parse', formData, { headers: { 'Content-Type': 'multipart/form-data', }, }); + // Parse locally after successful save + const text = await file.text(); + const parser = new RenPyParser(); + let tree; + try { + tree = parser.parse(text); + } catch (parseError) { + console.warn('[Frontend Parser] Local parse failed, returning empty tree:', parseError); + tree = {}; + } + return { script_id: response.data.script_id, filename: response.data.filename, tree: tree };backend/app/api/routes/scripts.py (1)
3-6:⚠️ Potential issue | 🟡 MinorRemove unused imports:
os,tempfile,json, andshutil.These imports are no longer referenced in the file. Parsing and file I/O logic have been moved to the frontend and database services, eliminating the need for these modules.
🤖 Fix all issues with AI agents
In `@backend/app/api/routes/scripts.py`:
- Around line 211-214: After mutations such as update_node_content and
insert_node the server currently omits any broadcast so other clients get stale
trees; add a lightweight "content_updated" broadcast after both
update_node_content and insert_node calls (or factor into a helper like
broadcast_content_updated) that sends minimal metadata (script_id, node_id,
user_id/timestamp) to connected clients; locate the mutation handlers in
scripts.py and invoke this new broadcast helper immediately after successful
mutation and commit so clients can re-fetch/re-parse the updated content.
- Around line 97-98: The catch-all except in backend/app/api/routes/scripts.py
currently converts all exceptions into a 500 by doing "except Exception as e:
raise HTTPException(...)", which swallows existing HTTPException errors; add an
explicit "except HTTPException: raise" clause immediately before the generic
"except Exception" in the same try/except block (the endpoint that currently
raises HTTPExceptions earlier in the function) so HTTPException instances raised
earlier (e.g., the 400/403 checks) are re-raised unchanged, leaving the final
catch-all to handle unexpected errors and wrap them as a 500.
In `@frontend/src/services/api.ts`:
- Around line 309-328: The loadExistingScript function is calling
/scripts/download and then re-parsing content locally; change apiClient.get in
loadExistingScript to fetch `/scripts/load/${scriptId}` and update its generic
response type to include {script_id: string, filename: string, content: string,
tree: any}; stop using RenPyParser.parse and instead use response.data.tree for
the returned tree, and ensure the returned object (ParsedScriptResponse) uses
script_id, filename, and tree from response.data (remove or keep parsing only as
a fallback if needed).
In `@frontend/src/services/RenPyParser.ts`:
- Around line 83-90: isElseStatement and isMenuStatement are too permissive
because startsWith("else")/startsWith("menu") will match longer identifiers like
"elsewhere:" or "menubar:"; update both functions to assert the keyword is a
separate token (use a regex with a word boundary) so only whole-word "else" and
"menu" followed by ":" match. Specifically, replace the body of isElseStatement
to use a pattern like /^\s*else\b.*:$/ (or equivalent token check) and replace
isMenuStatement to use /^\s*menu\b.*:$/ (or equivalent), keeping the trim/colon
check behavior; locate these changes in the functions named isElseStatement and
isMenuStatement in RenPyParser.ts.
🧹 Nitpick comments (6)
frontend/src/services/__tests__/RenPyParser.test.ts (2)
30-35:cleanGoldenis redefined on every test iteration.This function is identical across iterations. Hoist it next to
cleanNodefor clarity and to avoid re-creating closures per test.♻️ Proposed refactor
+function cleanGolden(node: any): any { + const { id, ...rest } = node; + if (rest.children) rest.children = rest.children.map(cleanGolden); + if (rest.false_branch) rest.false_branch = rest.false_branch.map(cleanGolden); + return rest; +} + describe('RenPyParser', () => { const parser = new RenPyParser(); Object.entries(goldenData).forEach(([caseName, data]: [string, any]) => { it(`should match golden data for case: ${caseName}`, () => { const result = parser.parse(data.content); const cleanedResult = cleanNode(result); - - // Remove IDs from golden data for comparison (just in case) - const cleanGolden = (node: any): any => { - const { id, ...rest } = node; - if (rest.children) rest.children = rest.children.map(cleanGolden); - if (rest.false_branch) rest.false_branch = rest.false_branch.map(cleanGolden); - return rest; - }; - const expected = cleanGolden(data.tree);
21-41: No edge-case tests for the parser.The suite only covers golden-path scenarios. Consider adding tests for empty input, content with no labels, scripts with only comments, tabs-vs-spaces indentation, and malformed blocks (e.g., missing colons). These would increase confidence in the parser's robustness, especially now that it's the sole source of truth for tree generation.
frontend/src/services/RenPyParser.ts (4)
26-26:String.prototype.substris deprecated; usesubstringinstead.♻️ Proposed fix
- this.id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + this.id = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
92-93:linesshould beprivate.
linesis internal parser state, only accessed by private parsing methods. Exposing it publicly leaks implementation details.♻️ Proposed fix
export class RenPyParser { - lines: string[] = []; + private lines: string[] = [];
243-249: Remove porting work-in-progress comments.These read as development notes from the Python→TypeScript port and will confuse future readers.
♻️ Proposed cleanup
if (isElifStatement(nextLineTrimmed)) { const falseBranchNode = new ChoiceNode("", index); - // In TS, false_branch is initialized to empty array. - // We need to push the new node to it, but parseStatement expects to fill currentNode.children/false_branch - - // Wait, Python logic: - // false_branch_node = ChoiceNode(start_line=index) - // index = self._parse_statement(index, false_branch_node, current_indent, ChoiceNodeType.IF_BLOCK) - // current_node.false_branch.append(false_branch_node) - index = this.parseStatement(index, falseBranchNode, currentIndent, ChoiceNodeType.IF_BLOCK);
36-55: Indentation assumes exactly 4-space tabs; mixed indentation has subtle behavior.
getIndentLevelhardcodes 4 spaces = 1 indent level, and a tab resets any partial space count. RenPy's default is 4 spaces, but if a script uses 2-space indentation, no blocks would be detected. This is fine as a design choice but worth a brief doc comment to make the assumption explicit.
| except Exception as e: | ||
| raise HTTPException(status_code=500, detail=f"Error parsing script: {str(e)}") | ||
| raise HTTPException(status_code=500, detail=f"Error saving script: {str(e)}") |
There was a problem hiding this comment.
Bug: HTTPException is swallowed and re-raised as 500.
Every other endpoint in this file has except HTTPException: raise before the catch-all except Exception. This endpoint is missing it, so HTTPExceptions raised at Lines 48, 51, 58, and 65 (status codes 400 and 403) will be caught here and re-raised as a generic 500 error.
🐛 Proposed fix
return result
+ except HTTPException:
+ raise
except Exception as e:
- raise HTTPException(status_code=500, detail=f"Error saving script: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Error saving script: {str(e)}") from e🧰 Tools
🪛 Ruff (0.14.14)
[warning] 97-97: Do not catch blind exception: Exception
(BLE001)
[warning] 98-98: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
[warning] 98-98: Use explicit conversion flag
Replace with conversion flag
(RUF010)
🤖 Prompt for AI Agents
In `@backend/app/api/routes/scripts.py` around lines 97 - 98, The catch-all except
in backend/app/api/routes/scripts.py currently converts all exceptions into a
500 by doing "except Exception as e: raise HTTPException(...)", which swallows
existing HTTPException errors; add an explicit "except HTTPException: raise"
clause immediately before the generic "except Exception" in the same try/except
block (the endpoint that currently raises HTTPExceptions earlier in the
function) so HTTPException instances raised earlier (e.g., the 400/403 checks)
are re-raised unchanged, leaving the final catch-all to handle unexpected errors
and wrap them as a 500.
| # Broadcast update | ||
| # Note: We no longer broadcast the full parsed tree because parsing is moved to frontend. | ||
| # Ideally, we should broadcast a "content_updated" event and let clients re-fetch or re-parse. | ||
| # For now, we omit the structure update broadcast. |
There was a problem hiding this comment.
Collaborative editing: no broadcast on content mutation.
The comment acknowledges that tree broadcasting was removed, but there's no replacement event (e.g., content_updated) to notify other connected clients that the script changed. After update_node_content and insert_node, collaborators will see stale trees until they manually reload. If collaborative editing is a supported feature, consider broadcasting a lightweight content_updated event so clients know to re-parse.
🤖 Prompt for AI Agents
In `@backend/app/api/routes/scripts.py` around lines 211 - 214, After mutations
such as update_node_content and insert_node the server currently omits any
broadcast so other clients get stale trees; add a lightweight "content_updated"
broadcast after both update_node_content and insert_node calls (or factor into a
helper like broadcast_content_updated) that sends minimal metadata (script_id,
node_id, user_id/timestamp) to connected clients; locate the mutation handlers
in scripts.py and invoke this new broadcast helper immediately after successful
mutation and commit so clients can re-fetch/re-parse the updated content.
| export const loadExistingScript = async (scriptId: string): Promise<ParsedScriptResponse> => { | ||
| const targetUrl = `${apiClient.defaults.baseURL}/scripts/load/${scriptId}`; | ||
| console.log(`[API Request] GET ${targetUrl} to load existing script`); | ||
| console.log(`[Frontend Logic] Loading script ${scriptId} and parsing locally...`); | ||
|
|
||
| // 1. Fetch content (using existing download endpoint logic) | ||
| try { | ||
| const response = await apiClient.get<ParsedScriptResponse>(`/scripts/load/${scriptId}`); | ||
| console.log('[API Response] loadExistingScript successful:', response.data); | ||
| return response.data; | ||
| const response = await apiClient.get<{content: string, filename: string}>(`/scripts/download/${scriptId}`); | ||
| const { content, filename } = response.data; | ||
| console.log(`[Frontend Logic] Content fetched for ${filename}. Size: ${content.length}`); | ||
|
|
||
| // 2. Parse locally | ||
| const parser = new RenPyParser(); | ||
| const tree = parser.parse(content); | ||
| console.log('[Frontend Logic] Local parsing successful.'); | ||
|
|
||
| // 3. Return result | ||
| return { | ||
| script_id: scriptId, | ||
| filename: filename, | ||
| tree: tree | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if /scripts/load is referenced anywhere in the frontend
rg -rn 'scripts/load' --type=ts --type=tsxRepository: the-asind/RenPy-VisualEditor
Length of output: 99
🏁 Script executed:
#!/bin/bash
# Search for /scripts/load in backend code (common backend file extensions)
rg -rn 'scripts/load' --type=py --type=go --type=js --type=ts --type=javaRepository: the-asind/RenPy-VisualEditor
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Examine the backend route definitions more broadly
rg -rn '/scripts' --type=py --type=go --type=js --type=ts -A 2 -B 2 | head -100Repository: the-asind/RenPy-VisualEditor
Length of output: 6948
🏁 Script executed:
#!/bin/bash
# Search for "load" endpoint in backend
rg -rn 'def load' --type=pyRepository: the-asind/RenPy-VisualEditor
Length of output: 130
🏁 Script executed:
#!/bin/bash
# Look at the scripts router file
head -100 backend/app/api/routes/scripts.pyRepository: the-asind/RenPy-VisualEditor
Length of output: 3569
🏁 Script executed:
#!/bin/bash
# Search for loadExistingScript function
rg -rn 'loadExistingScript' --type=tsRepository: the-asind/RenPy-VisualEditor
Length of output: 597
🏁 Script executed:
#!/bin/bash
# Read lines 300-350 of frontend api.ts to see the actual function
sed -n '300,350p' frontend/src/services/api.tsRepository: the-asind/RenPy-VisualEditor
Length of output: 1795
🏁 Script executed:
#!/bin/bash
# Get full backend scripts router file
cat backend/app/api/routes/scripts.pyRepository: the-asind/RenPy-VisualEditor
Length of output: 17462
🏁 Script executed:
#!/bin/bash
# Search for download endpoint
rg -n 'download' backend/app/api/routes/scripts.pyRepository: the-asind/RenPy-VisualEditor
Length of output: 277
🏁 Script executed:
#!/bin/bash
# Search for any references to /load or "load/" in frontend
rg -rn '\/load|load\/' frontend/ --type=tsRepository: the-asind/RenPy-VisualEditor
Length of output: 151
🏁 Script executed:
#!/bin/bash
# Check how loadExistingScript is used
rg -rn 'loadExistingScript' frontend/ --type=ts -A 5Repository: the-asind/RenPy-VisualEditor
Length of output: 2447
🏁 Script executed:
#!/bin/bash
# Search for "load" endpoint reference
rg -n 'load' frontend/src/services/api.ts | head -20Repository: the-asind/RenPy-VisualEditor
Length of output: 925
🏁 Script executed:
#!/bin/bash
# Check what's exported from api.ts
rg -n '^export const' frontend/src/services/api.tsRepository: the-asind/RenPy-VisualEditor
Length of output: 661
loadExistingScript should use /scripts/load instead of /scripts/download.
The backend's /scripts/load/{scriptId} endpoint returns {script_id, filename, content, tree: {}}, but loadExistingScript fetches from /scripts/download/{scriptId} which only returns {filename, content}. This leaves the /scripts/load endpoint unused. Switch to the /load endpoint to use the backend's intended response format, or remove the unused endpoint from the backend.
🤖 Prompt for AI Agents
In `@frontend/src/services/api.ts` around lines 309 - 328, The loadExistingScript
function is calling /scripts/download and then re-parsing content locally;
change apiClient.get in loadExistingScript to fetch `/scripts/load/${scriptId}`
and update its generic response type to include {script_id: string, filename:
string, content: string, tree: any}; stop using RenPyParser.parse and instead
use response.data.tree for the returned tree, and ensure the returned object
(ParsedScriptResponse) uses script_id, filename, and tree from response.data
(remove or keep parsing only as a fallback if needed).
| function isElseStatement(line: string): boolean { | ||
| return line.trim().startsWith("else") && line.trim().endsWith(":"); | ||
| } | ||
|
|
||
| function isMenuStatement(line: string): boolean { | ||
| const trimmed = line.trim(); | ||
| return trimmed.startsWith("menu") && trimmed.endsWith(":"); | ||
| } |
There was a problem hiding this comment.
isElseStatement and isMenuStatement can match unintended keywords.
startsWith("else") matches "elsewhere:", and startsWith("menu") matches "menubar:". While uncommon in RenPy scripts, a stricter check is safer.
🐛 Proposed fix
function isElseStatement(line: string): boolean {
- return line.trim().startsWith("else") && line.trim().endsWith(":");
+ const t = line.trim();
+ return t === "else:" || t.startsWith("else ") && t.endsWith(":");
}
function isMenuStatement(line: string): boolean {
const trimmed = line.trim();
- return trimmed.startsWith("menu") && trimmed.endsWith(":");
+ return trimmed === "menu:" || trimmed.startsWith("menu ") && trimmed.endsWith(":");
}🤖 Prompt for AI Agents
In `@frontend/src/services/RenPyParser.ts` around lines 83 - 90, isElseStatement
and isMenuStatement are too permissive because
startsWith("else")/startsWith("menu") will match longer identifiers like
"elsewhere:" or "menubar:"; update both functions to assert the keyword is a
separate token (use a regex with a word boundary) so only whole-word "else" and
"menu" followed by ":" match. Specifically, replace the body of isElseStatement
to use a pattern like /^\s*else\b.*:$/ (or equivalent token check) and replace
isMenuStatement to use /^\s*menu\b.*:$/ (or equivalent), keeping the trim/colon
check behavior; locate these changes in the functions named isElseStatement and
isMenuStatement in RenPyParser.ts.
Ported RenPyParser to TypeScript, added tests, updated API to use local parser, and removed backend parser logic.
PR created automatically by Jules for task 16989891955283388489 started by @the-asind
Summary by CodeRabbit
Refactor
Tests