Skip to content

Handle special character names on import (#138) + fix wizard-not-showing#249

Merged
mikkisguy merged 4 commits into
mainfrom
138-handle-special-character-names
Jun 22, 2026
Merged

Handle special character names on import (#138) + fix wizard-not-showing#249
mikkisguy merged 4 commits into
mainfrom
138-handle-special-character-names

Conversation

@mikkisguy

@mikkisguy mikkisguy commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #138. Adds CharacterNameType classification to detected characters so the import wizard can warn the user about variable references, Ren'Py interpolation, formatting tags, empty names, and the "???" placeholder. Also fixes a wizard-not-showing regression caused by PR #245 (#244).

Issue #138 — Special character names

When importing characters from Ren'Py projects, some character definitions use patterns the parser used to pass through raw to the UI:

  • Variable references (Character(boss_name, ...)) — unresolvable at import time
  • Ren'Py interpolation (Character("[e_name]", ...)) — runtime-dependent
  • Formatting tags (Character("{color=...}Stranger{/color}", ...)) — display-name noise
  • Special values (None, "", "???") — varying semantics
  • Non-ASCII (Japanese, CJK, emoji) — must round-trip unchanged

The fix adds a 7-state CharacterNameType classifier plus a stripRenpyTextTags() utility in packages/shared. The parser emits a typed record per character; the wizard renders warning badges and contextual helper text per type, but the user can still override the display name on import. The raw name is preserved verbatim for round-tripping.

Wizard-not-showing fix (follow-up to PR #245)

PR #245 (#244) auto-promotes extracted characters into the characters table during import. The pre-existing wizard trigger (newCharacters = characters.filter(c => !existingTagsSet.has(c.tag))) therefore returned an empty list on fresh imports and the wizard never opened.

The fix:

  • Show the wizard whenever detectionResult.characters.length > 0 in all four import flows.
  • For the secondary flows (ZipImportFilesDialog, GitLabSyncDialog), pass real existingTags so the new "Already imported" badge is meaningful on subsequent imports.
  • For the fresh-project flows (ZipImportProjectDialog, GitLabImportDialog), pass existingTags={[]} so the badge doesn't fire on the user's first review (PR fix(export): place generated branchforge_*.rpy under game/ and dedupe define/default symbols (#244) #245's auto-promote just put them there — the user hasn't reviewed yet).
  • The wizard's importCharacters endpoint already does upsert, so re-confirming unchanged rows is a safe no-op.

What changed

  • packages/shared/src/index.ts — new CharacterNameType type, stripRenpyTextTags() utility, nameType field on DetectedCharacter.
  • apps/backend/src/services/character-parser.service.ts — refactored parseFile to track nameForm (quoted/bracketed/identifier) and classify via classifyName(). Bracketed form preserves brackets in name for round-trip fidelity. Multi-line None followed by quoted options no longer overwrites the name.
  • apps/backend/src/lib/validation.ts — added nameType to detectedCharacterSchema.
  • apps/backend/src/services/gitlab-sync.service.tsnameType: "literal" default for the legacy rpy-parser path (documented as a known gap).
  • apps/frontend/src/components/CharacterImportWizard.tsx — warning badge + helper text per nameType, "(unnamed)" placeholder for empty names, "Already imported" badge, "(unnamed)" rendering in the special-character group.
  • apps/frontend/src/components/ide-shared/{ZipImportProject,ZipImportFiles,GitLabImport,GitLabSync}Dialog.tsx — wizard trigger updated, existingTags threaded through.

Verification

  • pnpm typecheck — passes
  • pnpm lint — passes
  • pnpm test:unit — 1659/1659 pass (40 new tests added)
  • Manual end-to-end: wizard now opens on fresh GitLab/ZIP imports with no spurious "Already imported" badges; re-sync flows show meaningful badges for existing rows.

Oracle review verdict

PASS (after addressing all must-fix and should-fix findings from the initial review). One should-fix item deferred with justification: the CharacterConflict shared type does not carry nameType, so the wizard's "Conflicts" group does not show name-type badges. The conflict path is rare (requires manual DB edits conflicting with parser output) and the conflict section already tells the user to "Review in character management after import" — a follow-up PR can add nameType to CharacterConflict if needed.

Summary by CodeRabbit

  • New Features
    • Enhanced import character detection to classify how each character name was defined (literal, variable, interpolated, tagged, none, empty, unknown) and show name-type warning badges in the import wizard.
    • Improved Ren’Py inline text-tag handling to ensure more accurate detection and display (including special-character fallbacks).
  • Bug Fixes
    • Updated GitLab/ZIP/script import flows so the character import wizard triggers and passes context more reliably based on detection results.
  • Tests
    • Added/expanded unit and UI tests covering name-type classification, badge rendering, and import behavior.

…nd warnings

- Added `nameType` classification to `DetectedCharacter` to differentiate between literal, variable, interpolated, tagged, empty, none, and unknown names.
- Implemented UI badges and helper text in the `CharacterImportWizard` to provide user guidance based on the `nameType`.
- Introduced `stripRenpyTextTags` function to handle Ren'Py text tags, ensuring proper display name extraction.
- Created unit tests for character parser service to validate name classification and display name derivation.
- Added tests for `CharacterImportWizard` to ensure correct rendering of badges and helper text based on character name types.
- Implemented tests for `stripRenpyTextTags` to verify tag stripping functionality and edge cases.
PR #245 (#244) auto-promotes extracted characters into the
`characters` table during GitLab/ZIP import, so the wizard's
`existingTags` filter (`newCharacters = characters.filter(c =>
!existingTagsSet.has(c.tag))`) returned an empty list on fresh
imports and the wizard never opened.

- Show the wizard whenever `detectionResult.characters.length > 0`
  in all four import flows (ZipImportProject, ZipImportFiles,
  GitLabImport, GitLabSync).
- Pass real `existingTags` to the wizard for the secondary
  re-sync flows (file-merge, GitLab sync) so the 'Already
  imported' badge is meaningful on subsequent imports.
- Pass `existingTags={[]}` from the two fresh-project dialogs
  (ZipImportProject, GitLabImport) so the badge doesn't fire on
  the user's first review of an import.
- Add an 'Already imported' badge in the wizard when a detected
  tag is in `existingTags`.
- Add a regression test using real-world RPY data
  (game/variables.rpy from a live project) covering
  `define x = Character('[var]')` and string-variable
  disambiguation.
@vercel

vercel Bot commented Jun 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
branchforge-docs Ready Ready Preview, Comment Jun 22, 2026 6:02pm

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mikkisguy, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 11 minutes and 3 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3f68979e-0630-4d5b-a2f2-b32e02687c10

📥 Commits

Reviewing files that changed from the base of the PR and between d3bc6c3 and 0b2ce63.

📒 Files selected for processing (1)
  • .changeset/rich-boxes-bow.md
📝 Walkthrough

Walkthrough

Adds a CharacterNameType union and stripRenpyTextTags utility to the shared package, extends the Ren'Py character parser with NameForm discrimination, classifyName, and buildDetectedCharacter helpers, propagates nameType through backend validation and sync, adds name-type warning badge UI to CharacterImportWizard, and changes all import dialogs to open the wizard for all detected characters rather than only new ones.

Changes

CharacterNameType classification and import wizard warnings

Layer / File(s) Summary
Shared types: CharacterNameType, stripRenpyTextTags, DetectedCharacter
packages/shared/src/index.ts, packages/shared/src/strip-renpy-tags.unit.test.ts
Exports CharacterNameType union (literal | variable | interpolated | tagged | none | empty | unknown), stripRenpyTextTags iterative tag-stripper, adds nameType field to DetectedCharacter with extended documentation, and unit tests the stripper.
Character parser: NameForm, classifyName, buildDetectedCharacter
apps/backend/src/services/character-parser.service.ts
Introduces NameForm discriminator and rawName in CharacterPatternMatch; adds extractColor, classifyName, and buildDetectedCharacter helpers; reworks single-line and multi-line parsing to resolve nameType/displayName/confidence and emit DetectedCharacter via buildDetectedCharacter; extends DetectedCharacter interface with nameType field; documents formatted names and semantics.
Backend validation schema and GitLab sync default
apps/backend/src/lib/validation.ts, apps/backend/src/services/gitlab-sync.service.ts
Adds nameType enum field with default "literal" to detectedCharacterSchema; adds nameType: "literal" to characters built in importFromGitlab with explanatory comments about parser limitations.
Backend parser unit and real-world tests
apps/backend/src/services/__tests__/character-parser.service.unit.test.ts, apps/backend/src/services/__tests__/character-parser-real-world.unit.test.ts
Adds tests for all nameType classifications, Ren'Py tag stripping, non-ASCII, multi-line parsing, color extraction, isSpecial/narrator regressions, deduplication, and two real-world .rpy fixture cases.
CharacterImportWizard nameType badge UI
apps/frontend/src/components/CharacterImportWizard.tsx
Adds getNameTypeBadge mapping nameType to badge label and helper text; adds conditional badge + AlertTriangle icon rendering in New Characters list; adds (unnamed) fallback for special characters; defaults existingTags to []; sets nameType: "literal" on prepared characters.
Import dialogs: show wizard for all detected characters
apps/frontend/src/components/ide-shared/GitLabImportDialog.tsx, apps/frontend/src/components/ide-shared/ZipImportFilesDialog.tsx, apps/frontend/src/components/ide-shared/ZipImportProjectDialog.tsx, apps/frontend/src/components/script-mode/GitLabSyncDialog.tsx
Removes new-only filtering before opening CharacterImportWizard; passes full detection result and existingTags (or []) to the wizard across all four import dialogs.
CharacterImportWizard tests
apps/frontend/src/components/__tests__/CharacterImportWizard.test.tsx
Adds RTL tests for nameType badge rendering per variant, import-payload round-tripping with user-edited displayName, excluded-state badge hiding, already-imported indicator, and special-character grouping with (unnamed) fallback.

Sequence Diagram(s)

sequenceDiagram
  participant ImportDialog as GitLabImportDialog / ZipImportDialog
  participant detectCharacters as charactersApi.detectCharacters
  participant CharacterImportWizard
  participant getNameTypeBadge
  participant importCharacters as charactersApi.importCharacters

  ImportDialog->>detectCharacters: detectCharacters(projectId)
  detectCharacters-->>ImportDialog: DetectedCharacter[] (with nameType)
  note over ImportDialog: opens wizard if characters.length > 0 (no new-only filter)
  ImportDialog->>CharacterImportWizard: characters, existingTags
  CharacterImportWizard->>getNameTypeBadge: char.nameType
  getNameTypeBadge-->>CharacterImportWizard: { label, helperText } | null
  note over CharacterImportWizard: renders badge + AlertTriangle when non-literal
  CharacterImportWizard->>importCharacters: { projectId, characters: [{ name, displayName, nameType }] }
  importCharacters-->>CharacterImportWizard: import result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • mikkisguy/branchforge#4: Modifies parseCharacterLine name-capture selection in character-parser.service.ts, which this PR refactors into the new NameForm-based classification model.
  • mikkisguy/branchforge#128: Touches CharacterImportWizard.tsx import handling logic, which this PR further extends with nameType data for badge rendering and import payload construction.

Poem

🐇 Hoppity-hop through the Ren'Py maze,
Where names can be tagged or wrapped in a haze!
literal, variable, interpolated too —
A badge on each character, just for you.
No more raw {color=#f00} shown as text,
The import wizard handles all the rest! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly references issue #138 (special character handling) and mentions the wizard regression fix, both primary objectives of this PR.
Linked Issues check ✅ Passed The PR fully implements all coding acceptance criteria from #138: variable names are classified, formatting tags are stripped, display names are editable, special values handled, non-ASCII preserved, and raw definition preserved for round-tripping.
Out of Scope Changes check ✅ Passed All changes directly support issue #138 and the wizard regression fix. Character parser enhancements, nameType classification, UI badge rendering, and import dialog corrections are all within stated objectives.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 138-handle-special-character-names

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.

@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

React Doctor found 6 issues in 5 files · 6 warnings · score 92 / 100 (Great) · vs main

6 warnings

src/components/CharacterImportWizard.tsx

  • ⚠️ L272 Large component is hard to read and change no-giant-component
  • ⚠️ L279 Empty default prop breaks memo rerender-memo-with-default-value

src/components/ide-shared/GitLabImportDialog.tsx

  • ⚠️ L142 Large component is hard to read and change no-giant-component

src/components/ide-shared/ZipImportFilesDialog.tsx

  • ⚠️ L132 Large component is hard to read and change no-giant-component

src/components/ide-shared/ZipImportProjectDialog.tsx

  • ⚠️ L118 Large component is hard to read and change no-giant-component

src/components/script-mode/GitLabSyncDialog.tsx

  • ⚠️ L123 Large component is hard to read and change no-giant-component

Reviewed by React Doctor for commit 0b2ce63. See inline comments for fixes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

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

Inline comments:
In `@apps/backend/src/services/character-parser.service.ts`:
- Around line 25-31: The comment for the `name` field in the
character-parser.service.ts file states that export uses the `name` field and
not `displayName`, but this contradicts what the shared `DetectedCharacter`
contract actually specifies. Update the comment for the `name` field to
accurately reflect which field the export functionality actually uses according
to the shared `DetectedCharacter` contract, ensuring the documentation aligns
with the actual implementation so callers have correct guidance.
- Around line 434-437: The regex pattern in the quotedNameMatch variable uses
`([^"]+)` which requires at least one character between quotes, causing empty
quoted strings like "" to not match and fall through to be classified as none.
Fix this by changing the quantifier from `+` to `*` in the regex pattern so it
matches zero or more characters, allowing empty quoted names to be properly
captured in multi-line definitions.
- Around line 343-368: The identifier fallback regex in the variableNameMatch
check is incorrectly capturing `None` as a regular identifier when it should be
treated as a special case. Before the existing identifier matching logic, add a
check to detect if the rest of the line starts with `None` and handle it
separately by setting the appropriate nameForm value to "none" instead of
allowing it to fall through to the identifier case. This ensures that
`Character(None,` on a single line is properly classified as a none value rather
than as an identifier.
- Around line 98-100: In the character-parser.service.ts file, separate the
imports of CharacterNameType and stripRenpyTextTags so that CharacterNameType
uses a type-only import statement while stripRenpyTextTags remains as a regular
import. Since CharacterNameType is only used in type positions (as the interface
property type in the service and the function return type), it should be
imported with the `import type` syntax to comply with TypeScript guidelines.
Remove the redundant type-only export statement for CharacterNameType since it
will be properly declared as a type-only import.

In `@apps/frontend/src/components/__tests__/CharacterImportWizard.test.tsx`:
- Line 14: The import statement for CharacterImportWizard uses a relative path
without a .js extension, which violates the repository's frontend import
conventions. Replace the relative import path with the `@/` alias and add the .js
extension to the import specifier. Change the CharacterImportWizard import to
use `@/components/CharacterImportWizard.js` format instead of the current relative
import.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0c406366-c237-4f1e-b727-67ce697a6fab

📥 Commits

Reviewing files that changed from the base of the PR and between 1baf1a2 and 13bdc75.

📒 Files selected for processing (13)
  • apps/backend/src/lib/validation.ts
  • apps/backend/src/services/__tests__/character-parser-real-world.unit.test.ts
  • apps/backend/src/services/__tests__/character-parser.service.unit.test.ts
  • apps/backend/src/services/character-parser.service.ts
  • apps/backend/src/services/gitlab-sync.service.ts
  • apps/frontend/src/components/CharacterImportWizard.tsx
  • apps/frontend/src/components/__tests__/CharacterImportWizard.test.tsx
  • apps/frontend/src/components/ide-shared/GitLabImportDialog.tsx
  • apps/frontend/src/components/ide-shared/ZipImportFilesDialog.tsx
  • apps/frontend/src/components/ide-shared/ZipImportProjectDialog.tsx
  • apps/frontend/src/components/script-mode/GitLabSyncDialog.tsx
  • packages/shared/src/index.ts
  • packages/shared/src/strip-renpy-tags.unit.test.ts

Comment thread apps/backend/src/services/character-parser.service.ts Outdated
Comment thread apps/backend/src/services/character-parser.service.ts Outdated
Comment thread apps/backend/src/services/character-parser.service.ts
Comment thread apps/backend/src/services/character-parser.service.ts
Comment thread apps/frontend/src/components/__tests__/CharacterImportWizard.test.tsx Outdated
@mikkisguy mikkisguy merged commit 13c8cf2 into main Jun 22, 2026
10 checks passed
detectedCharacters,
conflicts,
excludedTags,
existingTags = [],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

React Doctor · react-doctor/rerender-memo-with-default-value (warning)

This keeps redrawing children that compare props because default prop value [] makes a brand new array every render, so move it to a constant at the top of the file

Fix → Move it to the top of the file: const EMPTY_ITEMS: Item[] = [], then use that as the default value

Docs

@mikkisguy mikkisguy deleted the 138-handle-special-character-names branch June 22, 2026 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Design: Handle special characters with variable-based display names on import

1 participant