Repository navigation
Collaborative script editing: line and text editing on the draft (Phase 3, Step 3) - #1479
Conversation
…se 3, Step 3) Draft-aware line editing for client-v3's collaborative script editor, writing straight through useDraftWriter the moment each change happens rather than staging in local state: - useYTextField: binds a text input to a live Y.Text, keeping it in sync with remote edits and anchoring a focused caret to its character (via Yjs relative positions) rather than a raw index, so a concurrent edit earlier in the field doesn't make the caret jump. Distinguishes local vs remote writes by transaction origin so only remote edits need the caret actively restored. - CollabScriptLineEditor.vue / CollabScriptLinePart.vue: draft-aware siblings of the classic ScriptLineEditor/ScriptLinePart, since those are local-state and index-based with no Yjs awareness. Act/scene/character/style selects and part add/remove write directly through useDraftWriter; Vuelidate validation mirrors the classic rules. - CollabScriptEditor.vue: wires ScriptLineViewer's existing edit/insert/delete emits to the new editor, _uid-keyed open-editor tracking (no shift-reindexing needed on insert/delete, unlike the classic index-keyed version), and the Add Dialogue/Stage Direction/Cue Line/Spacing button group. D1 decided (Tim): incomplete lines keep the atomic abort on save (Step 4), with client-side Vuelidate gating added here so Save can disable itself with a reason — implemented in Step 4 since there's no Save button yet. Found while landing this step's own E2E, not a bug: once a line is actually edited the draft becomes dirty, and per D9 (Stop Editing keeps the draft) that persists to disk when the last editor leaves. has_unsaved_changes() then blocks turning collaborative_script_editing back off until the draft is discarded, and there's no Discard-draft UI yet (Step 5) — spec 15's own cleanup now calls the existing admin REST endpoint directly. Documented in the plan doc. Verified: 290 Vitest (+27), tsc/eslint/prettier/build clean, full Playwright 238/238 in both chromium and firefox, no retries. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
Client Test Results102 tests 102 ✅ 0s ⏱️ Results for commit 2b7ebca. ♻️ This comment has been updated with latest results. |
Client V3 Test Results299 tests 299 ✅ 1s ⏱️ Results for commit 2b7ebca. ♻️ This comment has been updated with latest results. |
Python Test Results 1 files 1 suites 1m 34s ⏱️ Results for commit 2b7ebca. ♻️ This comment has been updated with latest results. |
Playwright E2E Results (firefox)238 tests 238 ✅ 2m 29s ⏱️ Results for commit 2b7ebca. ♻️ This comment has been updated with latest results. |
Playwright E2E Results (chromium)238 tests 238 ✅ 2m 33s ⏱️ Results for commit 2b7ebca. ♻️ This comment has been updated with latest results. |
Quality gate failed on new-code duplication (10.9%, required <=3%) and a Reliability rating (missing input label). Fixes, no behaviour change: - Extract computeValidActs/computeValidScenes (src/js/scriptLineActScene.ts), characterSelectOptions/characterGroupSelectOptions/ stageDirectionStyleSelectOptions (src/js/scriptLinePartOptions.ts), and fieldState/combinedFieldState (src/js/vuelidateFieldState.ts) out of CollabScriptLineEditor.vue/CollabScriptLinePart.vue, which had copied these near-verbatim from the classic ScriptLineEditor.vue/ScriptLinePart.vue (same business rules, different write path). Pure functions, not touching the classic components. - Associate CollabScriptLinePart.vue's text input with a label (was missing one entirely after swapping BFormInput for a native <input> in Step 3). - Dedupe the two new component test files' stub boilerplate into testStubs.ts, and the two near-identical "add a dialogue line" E2E flows (spec 10's pre-existing one is untouched) behind a new fillOpenLineEditor() helper in e2e/helpers.ts. Verified: 290 Vitest, tsc/eslint/prettier/build clean, full Playwright 238/238 in both chromium and firefox, no retries. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
Tim020
left a comment
There was a problem hiding this comment.
Review round 1
Head SHA reviewed: cd77bac22703ffdcc1db71f396723325f8bb35ec
Verdict: changes needed. This is a well-structured step in the collaborative-editing rebuild (the Yjs text binding, room lifecycle, and reconnect handling are all solid), but there's one real functional regression and one real validation-correctness bug, plus a perf issue and several test gaps worth closing before merge.
Previous round follow-up: none — this is round 1.
SonarQube: Quality gate PASSED (all 5 conditions green: new reliability/security/maintainability ratings, duplication, hotspots-reviewed). 4 new MINOR code-smell issues were reported (none gate-blocking); all 4 are included as inline suggestions below (testStubs.ts:16, scriptLineActScene.ts:24/40/47). No security hotspots. Data confirmed fresh for this head SHA via anonymous SonarCloud API (MCP Sonar tools were unavailable — auth error on the MCP side only; anonymous API filled the gap cleanly since the project is public).
New findings: 2 Critical, 4 Important, 12 Suggestions (4 of which are SonarQube-sourced) posted inline, plus the non-line-anchored items below.
Findings without a specific diff line to anchor to:
- (Important)
scriptLineActScene.ts'scomputeValidActs/computeValidScenes— new act/scene chain-traversal logic extracted specifically to de-duplicate the classic and collaborative editors — has no test file at all. No test constructs a multi-act/multi-scene chain, so a traversal bug (e.g. a cycle vianext_act, orpreviousLine.act_idnot found inacts) wouldn't be caught. SuggestscriptLineActScene.test.ts. - (Important)
CollabScriptLinePart.vue's combined-character/group dropdown path (combinedValuegetter/setter, lines 201-216) is completely untested — no test ever setscharacter_combined_dropdown: true. This is also the exact path thetestStubs.tsNumber.parseIntissue (inline below) currently blocks from being tested. - (Important) The E2E spec's new Step-3 tests (
adds a dialogue line...,typed text survives a reload...,inserts a line from a row menu...) never use the second browser (page2) that the earliera second browser can edit...test opened — so the core claim of this step, "another editor sees it immediately" (per this PR's own doc comments), is never verified against a second real client over the actual WebSocket sync path, only in-process against a sharedY.DocinuseYTextField.test.ts. - (Suggestion)
CollabScriptEditor.vue's Add Stage Direction / Add Cue Line / Add Spacing buttons, and theinsert-dialogue/insert-cue-lineviewer emits, are untested — onlyaddNewLine(DIALOGUE)andinsertLineAt(STAGE_DIRECTION)are exercised. - (Suggestion)
scriptLinePartOptions.ts'scharacterGroupSelectOptionsandstageDirectionStyleSelectOptionsare never called by a test with real data. - (Suggestion)
useYTextField.test.ts's observer-switch test never confirms the oldY.Text's observer was actually removed (only that the new one works) — an observer leak wouldn't be caught. - (Suggestion)
CollabScriptLineEditor.vue:216-224/CollabScriptEditor.vue:218: opening a zero-part line's editor writes a new part as a side effect of mounting, and a reconnect resync that remounts an already-open editor re-grabs input focus. Both are narrow edge cases (concurrent part-removal; resync timing) but worth a second look. - (Suggestion)
useYTextField.ts's doc comment forinputRef("Bind this to the input element (ref=\"inputRef\")") prescribes a destructured top-level binding that the only real consumer,CollabScriptLinePart.vue, doesn't use (it assigns.valuemanually inonMountedinstead, since it needs its own separate template ref fordefineExpose). Worth updating the comment to show the pattern actually in use.
Strengths:
- The
useYTextFieldYjs binding itself is sound: local writes are tagged withLOCAL_EDIT_ORIGINso the observer skips redundant caret restoration, remote updates restore the caret by character position (not raw index) viacaptureSelection/resolveSelection, and the observer is cleanly removed inonUnmounted. CollabScriptEditor.vue's room/sync lifecycle tests (pre-existing from earlier steps) are unusually rigorous, several with comments naming the exact mutant they're designed to catch.- The E2E
afterAll's handling of the unsaved-draft-blocks-mode-flip interaction is a well-reasoned, documented workaround for a real sequencing constraint, not a hack. testStubs.ts's dedup of BVN component stand-ins across the three new test files is a clean, low-risk refactor.
🤖 Generated with Claude Code
Two real Criticals, found by review, not by me: 1. New lines never inherited the previous line's character. addLine() already seeds the first part in the same transaction, so CollabScriptLineEditor's onMounted seeding (guarded on line_parts.length === 0) never ran for a freshly-created line. Fixed by seeding the character directly in addNewLine/insertLineAt, the same lookup addLinePart() already does for a part added later. 2. combinedFieldState implemented OR-of-validity, not the XOR its own doc comment promised: a part with both character_id and character_group_id set (the state assertNotBoth forbids on write) reported as valid, since neither field had an active `required` rule once the other was set. Fixed at the source: a notBothCharacterFields validator on both fields, so $error correctly reflects the real invariant and the existing OR-of-no-error logic in combinedFieldState becomes correct automatically. Also addressed, all Important/Suggestion: - CollabScriptLinePart.vue's tmpScript conversion ran on every keystroke in every open part regardless of the MRU setting; gated behind it. - scriptLineActScene.ts/scriptLinePartOptions.ts's doc comments claimed classic+collab sharing that was never actually wired up; reworded to "mirrors the classic version, keep in sync" instead, and the latter's duplicate NamedItem/TmpScript types now reuse mruSortUtils.ts's (now exported) originals. - CollabScriptLineEditor.vue's actState/sceneState hand-rolled the logic the new fieldState() helper already exists for; now uses it. - useYTextField.ts had no IME composition handling (intermediate CJK/ dead-key state was written to the shared Y.Text, and a remote update mid-composition disrupted it); added compositionstart/compositionend handling, wired into CollabScriptLinePart.vue along with the missing @select caret-activity binding its own doc comment already promised. Also narrowed its `value` to Readonly<Ref<string>>. - The two new test files' stub boilerplate overlap is gone (testStubs.ts already existed); SelectStub now renders <optgroup> and passes non-numeric values through, enabling a real combined-dropdown test that didn't exist before; a test promising three emits only checked one, split into three; CollabScriptLineEditor.test.ts's PartStub was missing the stage-direction-style-change emit declaration, with no test exercising it or try-finish-line's lineValid guard — both added. - E2E: the cleanup DELETE's response went unchecked; now asserted. The helper's select.nth(0)/.nth(1) was a latent ambiguity; both line editors now carry data-testid="line-act-select"/"line-scene-select" and the helper uses those instead. Verified: 299 Vitest (+7), tsc/eslint/prettier/build clean, full Playwright 238/238 in both chromium and firefox, no retries. The two Critical fixes and the combined-dropdown/IME additions were each verified by reverting the fix and confirming the new test fails, then restoring it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
|
Addressed round 1 in Two real Criticals, found by review, not by me:
All Important/Suggestion items also addressed: the unconditional Verification: 299 Vitest (+7), tsc/eslint/prettier/build clean, full Playwright 238/238 in both chromium and firefox, no retries. The two Criticals and the new IME/combined-dropdown coverage were each verified by reverting the fix and confirming the new test fails, then restoring it. |
Tim020
left a comment
There was a problem hiding this comment.
Review round 2
Head SHA reviewed: 7186fc4fcedc297ca76215662fb1b8d68abef9c4
Verdict: ready to merge, pending the one small new finding below (optional — not gate-blocking).
Previous round follow-up: 18/18 threads verified fixed and resolved. All fixes were traced against the actual code (not just taken on the author's word) and, where the original finding was a logic/correctness issue, against the new tests added to cover them:
- Both Critical findings are genuinely fixed:
seedFirstPart()correctly seeds the character from the anchor line for bothaddNewLineandinsertLineAt(traced intodraftWriter.addLine'spartoption and itsassertNotBothcheck), andnotBothCharacterFieldsfixescombinedFieldState's invariant at the source (verified the Vuelidate v2 two-arg validator signature is used correctly) rather than patching the symptom. - All 4 Important findings fixed: the
tmpScriptperf issue is now correctly gated behind the MRU setting (no more phantom reactive dependency when it's off), both "shared between classic and collaborative" doc comments were rewritten to state the truth (duplication, not sharing) and verified still accurate against the unchanged classic components,NamedItem/TmpScriptduplication was resolved by exporting frommruSortUtils.ts, andactState/sceneStatenow use the sharedfieldState()helper. - All 12 Suggestions fixed, including two that turned into real new test coverage: the combined character/group dropdown is now actually testable (the stub needed
<optgroup>support, which the author discovered empirically) and has a test, and IME composition handling landed with two tests covering intermediate-state isolation and remote-edit-during-composition safety. - Full links: see the resolved threads on this PR.
SonarQube: Quality gate PASSED (all 5 conditions green). The 4 prior MINOR issues are gone. One new MAJOR issue appeared, introduced by this round's own fix (see inline comment) — not gate-blocking, but worth a quick cleanup. Source: anonymous SonarCloud API (public project), confirmed fresh for head 7186fc4.
New findings: 1 Suggestion (SonarQube-sourced), posted inline. No other new issues found in the fix commit — draftWriter.addLine's part handling, the Vuelidate sibling-validator usage, and the new insertLineAt test's component-lookup logic were all traced through and check out.
Strengths: Every fix was accompanied by a targeted regression test, and the commit message states each test was confirmed failing before the fix and passing after — genuine TDD discipline on a review response, not just code changes. The data-testid fix for the locator-ambiguity finding was applied to both line editors (collaborative and classic) rather than just the one in scope, keeping the shared E2E helper's doc comment honest.
🤖 Generated with Claude Code
SonarQube typescript:S3358, introduced by round 1's own fix (the stub needed to pass non-numeric combined-dropdown values through). Pulled the inner numeric check into parseSelectValue() so the outer expression no longer nests. Verified: 299 Vitest, tsc/eslint/prettier/build clean, full Playwright 238/238 in both chromium and firefox, no retries. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
|
Addressed round 2's one new finding in |
Tim020
left a comment
There was a problem hiding this comment.
Review round 3
Head SHA reviewed: 55b706d0ea94f93f800ab8e1b235b3858fcbedb1
Verdict: ready to merge.
Previous round follow-up: 1/1 verified fixed and resolved. The round-2 SonarQube finding (nested ternary in testStubs.ts) was fixed with exactly the suggested extraction — parseSelectValue() now holds the inner check, and the outer expression is a single non-nested ternary.
Correction, unrelated to this round's code: a script bug on my end meant the 18 verification replies I posted in round 2 were saved to GitHub with the literal text @- instead of their intended content (the replies themselves had already correctly driven thread resolution — only the displayed text was wrong). All 18 have now been edited in place with the correct verification text and a note explaining the correction. No action was needed from the PR author for this; flagging it here for the record.
SonarQube: Quality gate PASSED. 0 new issues (down from 1). Source: anonymous SonarCloud API (public project), confirmed fresh for head 55b706d.
New findings: none. This commit is a minimal, mechanical extraction — verified it's the only change in the diff, and all CI checks (SonarCloud, typecheck, lint, Python tests) are green on this head.
Strengths: Three clean rounds — two real Critical bugs and a long tail of Important/Suggestion findings, each fixed precisely as scoped with no regressions introduced, and the one regression this process did introduce (the nested ternary) was itself caught and fixed within a round. This PR is in good shape to merge.
🤖 Generated with Claude Code
…-v3-p3-step3-line-editing
|



Summary
useYTextFieldcomposable for caret-anchored live text binding,CollabScriptLineEditor.vue/CollabScriptLinePart.vueas draft-aware siblings of the classic editor components (writes go straight throughuseDraftWriterthe moment they happen), and_uid-keyed open-editor tracking plus add/insert/delete line inCollabScriptEditor.vue.collaborative_script_editingback off is blocked until the draft is discarded — there's no Discard-draft UI yet (Step 5). Spec 15's own test cleanup now calls the existing admin REST endpoint directly; documented in the plan doc as expected behaviour while the setting stays hidden.Test plan
tsc/eslint/prettier/buildclean🤖 Generated with Claude Code
https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8