Skip to content

feat: add YAML editor panel with conflict detection#1324

Merged
ggfevans merged 9 commits intomainfrom
feat/1323-yaml-editor-apply-flow
Feb 26, 2026
Merged

feat: add YAML editor panel with conflict detection#1324
ggfevans merged 9 commits intomainfrom
feat/1323-yaml-editor-apply-flow

Conversation

@ggfevans
Copy link
Collaborator

@ggfevans ggfevans commented Feb 26, 2026

User description

Summary

  • Extracts the YAML editor feature from dev/yamllama-ding-dong into a clean branch without unrelated auth removal, infra cleanup, or E2E rewrites
  • Adds LayoutYamlPanel component with read-only/edit modes, debounced Zod schema validation, and conflict detection when layout changes externally during editing
  • Wires into File menu (desktop), mobile bottom sheet, and dialog store
  • Removes deprecated slot_width and slot_position from YAML serialisation (NetBox 3.8+)
  • Hardens async flow: fixes hydration race in syncFromLayout, preserves conflict state on apply failure, adds intent guards to prevent stale/duplicate applies
  • Adds hasRacks guard to MobileFileSheet Share and View YAML buttons (parity with FileMenu)
  • Adds handleFitAll() to YAML editor close handlers for consistency with other dialog patterns

Closes #1323
Implements #1176

Test plan

  • Open app, click File > View YAML — read-only YAML appears
  • Click "Edit YAML", make invalid change — validation error shown, Apply disabled
  • Fix YAML — validation clears, Apply enabled
  • Apply valid YAML — layout updates, editor resets to read-only
  • Modify layout externally while editor is open, then Apply — conflict prompt appears
  • "Apply anyway" works; rapid double-clicks don't cause duplicate applies
  • If apply callback fails, conflict state preserved for retry (error toast shown)
  • Mobile: open File sheet > View YAML — bottom sheet with YAML editor
  • Mobile: Share and View YAML disabled when no racks exist
  • npm run lint — no errors
  • npm run test:run — all 1880 tests pass (87 files)
  • npm run build — production build succeeds

🤖 Generated with Claude Code


CodeAnt-AI Description

Add editable Layout YAML panel with validation, conflict detection, and UI integrations

What Changed

  • Users can open a read-only or editable "Layout YAML" view from the File menu, toolbar, and mobile file sheet; on mobile the editor appears in a bottom sheet.
  • The YAML editor shows syntax and schema validation messages, disables Apply until YAML is valid, and provides copy and download actions.
  • If the layout changes elsewhere while the editor is open, the editor shows a conflict prompt with options to reload the latest layout or "Apply anyway"; applying preserves selection clearing, marks the layout dirty, and shows a success or error toast.
  • YAML serialization omits deprecated NetBox fields (slot_width and slot_position); round-trip tests updated and new unit tests cover validation, conflict flow, and mobile behaviours.
  • File and mobile sheets disable Share and View YAML when no racks exist; rapid double-clicks on "Apply anyway" are guarded against duplicate applies.

Impact

✅ Clearer YAML validation and errors
✅ Fewer accidental overwrites from concurrent edits
✅ Consistent mobile and desktop access to YAML

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

ggfevans and others added 2 commits February 25, 2026 19:27
Extract the YAML editor feature from dev/yamllama-ding-dong into a clean
implementation. Adds LayoutYamlPanel component with read-only/edit modes,
debounced Zod schema validation, and conflict detection when the layout
changes externally during editing.

- Wire into dialog store, File menu, Toolbar, mobile bottom sheet
- Remove deprecated slot_width/slot_position from YAML serialisation
- Update yaml-roundtrip test for removed fields

Closes #1323
Implements #1176

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add analytics tracking to handleViewYaml toolbar handler
- Fix redundant min(100%, 100%) CSS rule in LayoutYamlPanel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 26, 2026

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a YAML editor feature: new LayoutYamlPanel component, UI wiring to open it from File menu/toolbar/mobile sheet (dialog + bottom sheet), App.svelte handlers to open/close/apply YAML edits, dialog/sheet type updates, tests for the editor, and removal of deprecated slot_width/slot_position from YAML utilities.

Changes

Cohort / File(s) Summary
App & UI wiring
src/App.svelte
Added yaml editor dialog/sheet state and handlers (open/close/apply), imported Layout type, and applied layout updates via loadLayout, markDirty, clearSelection, toast, and requestAnimationFrame fit on apply.
Toolbar & Menus (desktop + mobile)
src/lib/components/Toolbar.svelte, src/lib/components/FileMenu.svelte, src/lib/components/MobileFileSheet.svelte
Added onviewyaml?: () => void prop across components; wired "View YAML" menu item/button (disabled when no racks), analytics hook, and propagation from toolbar → file menu → app.
YAML editor component
src/lib/components/LayoutYamlPanel.svelte
New component: read-only/edit modes, debounced YAML parse + Zod schema validation, conflict detection (reload vs apply anyway), apply flow via onapply, copy/download, toasts, and validation/conflict UI.
Dialog/sheet types & delete target
src/lib/stores/dialogs.svelte.ts
Added yamlEditor to DialogId and SheetId; expanded DeleteTarget to include required name: string.
YAML utilities
src/lib/utils/yaml.ts
Removed slot_width and slot_position from ordering/population logic and updated related comments/tests to omit those fields from serialized YAML.
Tests
src/tests/LayoutYamlPanel.test.ts, src/tests/yaml-roundtrip.test.ts, src/tests/MobileFileSheet.test.ts
Added LayoutYamlPanel tests (validation, conflict flow, apply); updated yaml-roundtrip expectations to assert omission of slot_width/slot_position; updated MobileFileSheet tests to support hasRacks and onviewyaml scenarios.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant FileMenu as FileMenu
    participant App as App.svelte
    participant Dialog as Dialog/Sheet
    participant Editor as LayoutYamlPanel
    participant Store as LayoutStore

    User->>FileMenu: Click "View YAML"
    FileMenu->>App: onviewyaml()
    App->>Dialog: open yamlEditor (dialog or sheet) with current layout
    Dialog->>Editor: render baseline YAML
    User->>Editor: Edit YAML
    Editor->>Editor: Debounced parse & Zod validation
    alt Valid YAML
        Editor->>User: Enable Apply
    else Invalid YAML
        Editor->>User: Show validation errors
    end
    alt Concurrent change detected
        Editor->>User: Prompt "Reload latest" or "Apply anyway"
        User->>Editor: Choose action
    end
    User->>Editor: Click Apply
    Editor->>App: onapply(updatedLayout)
    App->>Store: loadLayout(updatedLayout)
    App->>App: requestAnimationFrame -> handleFitAll()
    App->>Dialog: close yamlEditor
    App->>User: Show success toast
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐇 I nibble keys and watch YAML grow,
Lines align where rack-data flow,
Debounced checks hum soft and clear,
Conflicts bow, then "Apply" draws near,
Hopping off with a crunchy carrot cheer 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add YAML editor panel with conflict detection' accurately summarizes the main feature addition: a YAML editor with conflict detection capability.
Linked Issues check ✅ Passed The PR implements all requirements from #1323 and #1176: creates LayoutYamlPanel with read-only/edit modes, debounced validation, conflict detection, wires into File menu/toolbar/mobile, removes deprecated NetBox fields, and adds comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are directly related to the YAML editor feature. File modifications match the scope defined in #1323: component creation, dialog store updates, menu/toolbar/mobile wiring, YAML field removals, and tests.
Description check ✅ Passed The pull request description clearly relates to the changeset. It explains the addition of a YAML editor feature, LayoutYamlPanel component, validation, conflict detection, and removal of deprecated fields—all present in the changes.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/1323-yaml-editor-apply-flow

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.

@codeant-ai codeant-ai bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Feb 26, 2026
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 26, 2026

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • Apply handler
    The new handleYamlApply replaces the entire layout via layoutStore.loadLayout(nextLayout) and marks the layout dirty. Confirm this correctly preserves required derived state (ids, image references). Ensure any conflict resolution or validation is performed prior to calling this handler to avoid partial/invalid state being applied.

  • Data Loss Risk
    The placed-device and device-type ordering functions no longer emit slot_position/slot_width. Ensure that when importing YAML containing those deprecated fields they are either preserved in runtime state (if needed) or explicitly dropped with awareness — otherwise users could lose placement data on round-trips.

  • Backwards Compatibility
    The YAML serializer intentionally omits deprecated NetBox fields (slot_width, slot_position). Verify how older YAML files containing those keys will be handled on import: the parser/validation path should either accept and map/remove deprecated keys before validation or provide a clear migration path to avoid rejecting older files.

  • Dialog wiring
    A Dialog + LayoutYamlPanel and a mobile BottomSheet variant were added. Verify the dialog/sheet open/close lifecycle is consistent with dialogStore (no double-sources-of-truth) and that the editor resets or reinitialises correctly if the underlying layout changes while the editor is open.

  • New dialog/sheet IDs
    yamlEditor was added to both DialogId and SheetId. Review all consumers of these union types to ensure they handle the new IDs (open/close, serialization, UI wiring). Missing handling could allow dead paths or runtime state confusion.

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

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

Inline comments:
In `@src/lib/components/LayoutYamlPanel.svelte`:
- Around line 62-65: The effect currently bails out or drops sync results if
`isEditing` flips true during the async `syncFromLayout` call, causing a
hydration race that leaves `yamlText` empty when the user quickly enters edit
mode; modify the `$effect` that calls `syncFromLayout(layout, {
allowWhileEditing: false })` so it snapshots `isEditing` (e.g. `const
editingAtStart = isEditing`) before awaiting the async call and only ignores the
result if `editingAtStart` was true (not if `isEditing` changed while the
promise was in flight); keep the `isApplying` and `open` guards, and apply the
same snapshotting fix where `syncFromLayout` is used elsewhere (the other blocks
around the same area).
- Around line 189-200: The commitParsedLayout function currently clears conflict
state and pendingLayout in the finally block even when onapply or syncFromLayout
throws; change it to catch errors from await onapply?.(nextLayout) and await
syncFromLayout(nextLayout), set isApplying = false in a finally, but only clear
showConflictPrompt, latestYamlAtConflict, pendingLayout and set isEditing =
false when the apply/sync succeeds; on failure keep those conflict/pending flags
intact, surface the error to the user (e.g., set/emit an error or call an
existing notification handler) and rethrow or return so callers can react.
Ensure you update references to commitParsedLayout, onapply, syncFromLayout,
isApplying, isEditing, showConflictPrompt, latestYamlAtConflict, and
pendingLayout when implementing this flow.
- Around line 203-223: handleApply can continue after async awaits even if the
user cancelled/closed the panel; add an intent guard to abort stale flows:
introduce a component-level numeric token (e.g. applyIntentId) that you
increment at the start of handleApply, capture it in a local const (e.g. const
intent = applyIntentId) and after each await (after parseLayoutYaml and
serializeLayoutToYaml) check if intent !== applyIntentId and return early if so;
apply the same guard before setting
pendingLayout/latestYamlAtConflict/showConflictPrompt and before calling
commitParsedLayout(parsedLayout).

In `@src/tests/LayoutYamlPanel.test.ts`:
- Around line 92-144: The test replaces yamlUtils.serializeLayoutToYaml with
vi.spyOn but never restores it, risking test pollution; fix by restoring the spy
after the test (either call the spy's mockRestore() or invoke
vi.restoreAllMocks()) — e.g., capture the spy returned from vi.spyOn(yamlUtils,
"serializeLayoutToYaml") and call spy.mockRestore() at the end of the it block
or add an afterEach(() => vi.restoreAllMocks()) in the describe that contains
this test so serializeLayoutToYaml is returned to its original implementation
for subsequent tests.

In `@src/tests/yaml-roundtrip.test.ts`:
- Around line 41-44: The test currently uses exact-length assertions
(expect(restored.racks).toHaveLength(1) and
expect(restored.device_types).toHaveLength(1)) which violates the ESLint rule;
update the assertions in yaml-roundtrip.test.ts around parseLayoutYaml to assert
existence/non-emptiness instead (e.g., assert Array.isArray(restored.racks) and
restored.racks.length > 0 and similarly for restored.device_types, or use
expect(restored.racks).toBeDefined() plus
expect(restored.racks.length).toBeGreaterThan(0)) so the round-trip validates
presence without using exact length checks.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e0efb9 and aaa670b.

📒 Files selected for processing (9)
  • src/App.svelte
  • src/lib/components/FileMenu.svelte
  • src/lib/components/LayoutYamlPanel.svelte
  • src/lib/components/MobileFileSheet.svelte
  • src/lib/components/Toolbar.svelte
  • src/lib/stores/dialogs.svelte.ts
  • src/lib/utils/yaml.ts
  • src/tests/LayoutYamlPanel.test.ts
  • src/tests/yaml-roundtrip.test.ts

Comment on lines 928 to 936
function handleYamlApply(nextLayout: Layout) {
layoutStore.loadLayout(nextLayout);
layoutStore.markDirty();
selectionStore.clearSelection();
toastStore.showToast("YAML applied", "success");

requestAnimationFrame(() => {
handleFitAll();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: After successfully applying valid YAML, the layout is updated and the canvas re-fitted but the YAML dialog/sheet remains open, which contradicts the stated behavior in the PR test plan ("Apply valid YAML — layout updates, editor closes") and forces users to dismiss the editor manually instead of closing it automatically in both desktop and mobile flows. [logic error]

Severity Level: Major ⚠️
- Critical: LayoutYamlPanel dialog or sheet remains open post-apply.
- Warning: YAML editor UX diverges from documented test plan.
Suggested change
function handleYamlApply(nextLayout: Layout) {
layoutStore.loadLayout(nextLayout);
layoutStore.markDirty();
selectionStore.clearSelection();
toastStore.showToast("YAML applied", "success");
requestAnimationFrame(() => {
handleFitAll();
});
function handleYamlApply(nextLayout: Layout) {
layoutStore.loadLayout(nextLayout);
layoutStore.markDirty();
selectionStore.clearSelection();
toastStore.showToast("YAML applied", "success");
if (viewportStore.isMobile) {
dialogStore.closeSheet();
} else {
dialogStore.close();
}
requestAnimationFrame(() => {
handleFitAll();
});
}
Steps of Reproduction ✅
1. Open the app and ensure a layout is present (create or load one) so the toolbar is
active; this uses `App.svelte` lines 1567-1583 where `<Toolbar ...
onviewyaml={handleOpenYamlEditor} />` is wired.

2. On desktop, click the File/toolbar action that triggers "View YAML"; this calls
`handleOpenYamlEditor` in `src/App.svelte:911-918`, which opens the YAML editor dialog by
calling `dialogStore.open("yamlEditor")`, causing the `<Dialog>` with `<LayoutYamlPanel
... onapply={handleYamlApply} />` at `App.svelte:1781-1792` to render. On mobile, the same
action from `MobileFileSheet` at `src/App.svelte:1830-1837` opens the YAML editor bottom
sheet at `App.svelte:1842-1852`.

3. In the YAML editor UI (`LayoutYamlPanel.svelte`), click "Edit YAML", enter valid YAML,
and click "Apply YAML"; this triggers `handleApply` at
`src/lib/components/LayoutYamlPanel.svelte:203-223`, which parses the YAML and then calls
`await commitParsedLayout(parsedLayout)`, and `commitParsedLayout` at lines 189-200
invokes the `onapply` callback provided by the parent.

4. The `onapply` callback is `handleYamlApply` in `src/App.svelte:928-937`, which loads
the new layout, marks it dirty, clears selection, shows the "YAML applied" toast, and
refits the canvas via `requestAnimationFrame`, but never calls `dialogStore.close()` or
`dialogStore.closeSheet()`, so `yamlEditorDialogOpen` / `yamlEditorSheetOpen` (derived at
`App.svelte:145-162`) remain true and the YAML editor dialog or bottom sheet stays open
after applying, contrary to the PR test plan step "Apply valid YAML — layout updates,
editor closes".
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/App.svelte
**Line:** 928:936
**Comment:**
	*Logic Error: After successfully applying valid YAML, the layout is updated and the canvas re-fitted but the YAML dialog/sheet remains open, which contradicts the stated behavior in the PR test plan ("Apply valid YAML — layout updates, editor closes") and forces users to dismiss the editor manually instead of closing it automatically in both desktop and mobile flows.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

Comment on lines +62 to +65
$effect(() => {
if (!open || isEditing || isApplying) return;
void syncFromLayout(layout, { allowWhileEditing: false });
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Closing the YAML editor while in edit mode does not reset its internal editing state, so reopening it shows stale in-progress edits instead of a fresh snapshot of the current layout; add an effect that resets editing-related state when open becomes false so each new open starts from the latest layout. [logic error]

Severity Level: Major ⚠️
- ⚠️ YAML editor reopens showing stale unsaved YAML content.
- ⚠️ Applying YAML can overwrite newer layout modifications.
- ⚠️ Conflict detection baseline may not match current layout.
Suggested change
$effect(() => {
if (!open || isEditing || isApplying) return;
void syncFromLayout(layout, { allowWhileEditing: false });
});
$effect(() => {
if (!open || isEditing || isApplying) return;
void syncFromLayout(layout, { allowWhileEditing: false });
});
$effect(() => {
if (!open) {
// Reset editing session when the panel is closed so reopening starts fresh
isEditing = false;
showConflictPrompt = false;
pendingLayout = null;
latestYamlAtConflict = null;
syntaxError = null;
schemaError = null;
isValidating = false;
}
});
Steps of Reproduction ✅
1. Start the app with the PR code and open the YAML editor via the File > View YAML menu
or toolbar "View YAML" action, which calls `handleOpenYamlEditor()` in
`src/App.svelte:911-918` and opens the `<Dialog>` containing `<LayoutYamlPanel
open={yamlEditorDialogOpen} ...>` at `src/App.svelte:1781-1791` (desktop) or the
`<BottomSheet>` with `<LayoutYamlPanel open={yamlEditorSheetOpen} ...>` at
`src/App.svelte:1842-1852` (mobile), setting the `open` prop to `true`.

2. In `LayoutYamlPanel.svelte`, the first `$effect` at `lines 62-65` runs because `open
=== true`, `isEditing === false`, and `isApplying === false`, invoking
`syncFromLayout(layout, { allowWhileEditing: false })` which sets `baselineYaml` and
`yamlText` to the current layout and clears validation state.

3. Click the "Edit YAML" button in the panel header (the button wired to
`onclick={handleEditModeToggle}` at `LayoutYamlPanel.svelte:264-270`), which sets
`isEditing = true` in `handleEditModeToggle()` at `LayoutYamlPanel.svelte:160-168`, then
type arbitrary changes into the `<textarea data-testid="yaml-textarea">` at
`LayoutYamlPanel.svelte:310-317` without clicking "Cancel edits" or "Apply YAML", leaving
unsaved in-progress edits in `yamlText` and `isEditing === true`.

4. Close the YAML editor without using "Cancel edits": on desktop, close the dialog via
its close control or ESC so `handleYamlEditorClose()` in `src/App.svelte:920-922` calls
`dialogStore.close()`, making `yamlEditorDialogOpen` false; on mobile, close the sheet so
`handleYamlEditorSheetClose()` at `src/App.svelte:924-926` calls
`dialogStore.closeSheet()`, making `yamlEditorSheetOpen` false. In both cases,
`LayoutYamlPanel`'s `open` prop becomes `false`, but there is no `$effect` that resets
`isEditing`, `yamlText`, or other editing state on close, so those variables retain their
previous values.

5. Reopen the YAML editor again via the same menu/button so `open` becomes `true` once
more for `LayoutYamlPanel`. The first `$effect` at `LayoutYamlPanel.svelte:62-65` now sees
`open === true` but `isEditing === true` (carried over from the prior session) and
immediately returns without calling `syncFromLayout`, so `baselineYaml`, `yamlText`, and
validation flags are not refreshed from the latest `layout`. The textarea shows the stale
unsaved edits from the prior session instead of a fresh snapshot of `layout`, and if
`layoutStore.layout` has changed in the meantime (e.g., racks/devices modified through the
main UI in `App.svelte`), pressing "Apply YAML" will parse and apply this stale YAML in
`handleApply()` at `LayoutYamlPanel.svelte:205-223`, potentially overwriting newer layout
changes or producing misleading conflict prompts.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/lib/components/LayoutYamlPanel.svelte
**Line:** 62:65
**Comment:**
	*Logic Error: Closing the YAML editor while in edit mode does not reset its internal editing state, so reopening it shows stale in-progress edits instead of a fresh snapshot of the current layout; add an effect that resets editing-related state when `open` becomes false so each new open starts from the latest layout.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

}: Props = $props();

function handleAction(action?: () => void) {
action?.();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: On mobile, tapping the "View YAML" action briefly opens the YAML sheet and then immediately closes it because handleAction opens the new sheet and then calls onclose, which closes whatever sheet is currently open (now the YAML editor), so the user never sees the YAML editor. [logic error]

Severity Level: Critical 🚨
- ❌ Mobile users cannot open Layout YAML editor sheet.
- ⚠️ New YAML editor feature unusable from mobile File menu.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 26, 2026

CodeAnt AI finished reviewing your PR.

ggfevans and others added 2 commits February 25, 2026 20:08
Snapshot isEditing before await in syncFromLayout to prevent hydration
race, preserve conflict state on apply failure, add intent guard to
handleApply against stale data, restore spied mocks in tests, and
replace exact-length assertions in roundtrip test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add isApplying check and applyIntentId bump to prevent rapid
double-clicks on "Apply anyway" from triggering concurrent commits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 `@src/lib/components/LayoutYamlPanel.svelte`:
- Around line 239-248: The reload flow in handleReloadLatest may complete with
stale state if user actions occur while await serializeLayoutToYaml(layout) is
in flight; add an intent guard by capturing a local intent/token or snapshot of
relevant state (e.g., latestYamlAtConflict and/or pendingLayout) before the
await and compare it immediately after the await to ensure the user still
intends this reload; if the token/state changed, abort without mutating
baselineYaml, yamlText, showConflictPrompt, latestYamlAtConflict, pendingLayout,
or isEditing.
- Around line 1-23: Add debug logging using the debug package with namespace
"rackula:layout:state": import and instantiate the debugger at top of
LayoutYamlPanel.svelte, then insert debug calls at key operations — inside
syncFromLayout (e.g., "syncing from layout, runId=%d" with runId), inside
validateYamlText (e.g., "validation complete, hasErrors=%o" with
syntaxError/schemaError state), and inside handleApply (e.g., "apply started,
intent=%d" and "conflict detected, baselineYaml differs from latestYaml") so the
async flows (sync, validation, conflict detection) emit namespace-filtered
diagnostics; use the existing function/variable names syncFromLayout,
validateYamlText, handleApply, runId, syntaxError, schemaError, baselineYaml and
latestYaml to place the messages.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38dbc25 and 6252184.

📒 Files selected for processing (1)
  • src/lib/components/LayoutYamlPanel.svelte

Comment on lines 1 to 23
<script lang="ts">
import type { ZodIssue } from "zod";
import type { Layout } from "$lib/types";
import { LayoutSchema } from "$lib/schemas";
import { buildYamlFilename } from "$lib/utils/folder-structure";
import {
parseLayoutYaml,
parseYaml,
serializeLayoutToYaml,
} from "$lib/utils/yaml";
import { getToastStore } from "$lib/stores/toast.svelte";
import { IconCopy, IconDownload } from "./icons";
import { ICON_SIZE } from "$lib/constants/sizing";

interface Props {
open: boolean;
layout: Layout;
onapply?: (layout: Layout) => void | Promise<void>;
}

let { open, layout, onapply }: Props = $props();

const toastStore = getToastStore();
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add debug logging for YAML editor operations.

Per coding guidelines, use the debug npm package with namespace filtering for debug logging. This component handles complex async flows (sync, validation, conflict detection) where debug logging would aid troubleshooting.

Proposed addition
 <script lang="ts">
   import type { ZodIssue } from "zod";
   import type { Layout } from "$lib/types";
   import { LayoutSchema } from "$lib/schemas";
   import { buildYamlFilename } from "$lib/utils/folder-structure";
   import {
     parseLayoutYaml,
     parseYaml,
     serializeLayoutToYaml,
   } from "$lib/utils/yaml";
   import { getToastStore } from "$lib/stores/toast.svelte";
   import { IconCopy, IconDownload } from "./icons";
   import { ICON_SIZE } from "$lib/constants/sizing";
+  import { layoutDebug } from "$lib/utils/debug";
+
+  const debug = layoutDebug.extend("yaml-panel");

Then use in key operations:

// In syncFromLayout
debug("syncing from layout, runId=%d", runId);

// In validateYamlText
debug("validation complete, hasErrors=%o", { syntaxError, schemaError });

// In handleApply
debug("apply started, intent=%d", intent);
debug("conflict detected, baselineYaml differs from latestYaml");

As per coding guidelines: "Use the debug npm package with namespace filtering for debug logging... Use established namespaces: rackula:layout:state..."

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

In `@src/lib/components/LayoutYamlPanel.svelte` around lines 1 - 23, Add debug
logging using the debug package with namespace "rackula:layout:state": import
and instantiate the debugger at top of LayoutYamlPanel.svelte, then insert debug
calls at key operations — inside syncFromLayout (e.g., "syncing from layout,
runId=%d" with runId), inside validateYamlText (e.g., "validation complete,
hasErrors=%o" with syntaxError/schemaError state), and inside handleApply (e.g.,
"apply started, intent=%d" and "conflict detected, baselineYaml differs from
latestYaml") so the async flows (sync, validation, conflict detection) emit
namespace-filtered diagnostics; use the existing function/variable names
syncFromLayout, validateYamlText, handleApply, runId, syntaxError, schemaError,
baselineYaml and latestYaml to place the messages.

Comment on lines 239 to 248
async function handleReloadLatest(): Promise<void> {
const latest =
latestYamlAtConflict ?? (await serializeLayoutToYaml(layout));
baselineYaml = latest;
yamlText = latest;
showConflictPrompt = false;
latestYamlAtConflict = null;
pendingLayout = null;
isEditing = true;
}
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 adding an intent guard after the async await.

If the user rapidly triggers other actions while serializeLayoutToYaml is in flight at line 241, the reload could complete with stale assumptions. This is low-risk since it's an explicit user action, but for consistency with handleApply, an intent guard would be more robust.

Optional enhancement
   async function handleReloadLatest(): Promise<void> {
+    const intent = ++applyIntentId;
     const latest =
       latestYamlAtConflict ?? (await serializeLayoutToYaml(layout));
+    if (intent !== applyIntentId) return;
     baselineYaml = latest;
     yamlText = latest;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/components/LayoutYamlPanel.svelte` around lines 239 - 248, The reload
flow in handleReloadLatest may complete with stale state if user actions occur
while await serializeLayoutToYaml(layout) is in flight; add an intent guard by
capturing a local intent/token or snapshot of relevant state (e.g.,
latestYamlAtConflict and/or pendingLayout) before the await and compare it
immediately after the await to ensure the user still intends this reload; if the
token/state changed, abort without mutating baselineYaml, yamlText,
showConflictPrompt, latestYamlAtConflict, pendingLayout, or isEditing.

- Add handleFitAll to YAML editor close handlers for consistency with
  other dialog close patterns (share, help, view sheet)
- Add hasRacks prop to MobileFileSheet and disable Share/View YAML
  buttons when no racks exist, matching FileMenu behaviour
- Add clarifying comment on conflict test mock strategy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/tests/MobileFileSheet.test.ts (1)

87-96: 🧹 Nitpick | 🔵 Trivial

Add tests for the new View YAML action path.

This update covers Share with hasRacks, but it doesn’t add assertions for the newly introduced View YAML behavior (enabled/disabled state and callback+close flow). Please add that user-facing coverage.

As per coding guidelines, “Test only high-value behavior … user-facing behavior … error paths and edge cases.”

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

In `@src/tests/MobileFileSheet.test.ts` around lines 87 - 96, Add user-facing
tests for the new "View YAML" action: after calling renderSheet({ hasRacks: true
}), assert the "View YAML" button is enabled (getByRole("button", { name: "View
YAML" })), simulate a click with fireEvent.click and assert the onViewYAML (or
onViewYaml) callback is called once and onClose is called once while
onLoad/onSave/onExport/onShare remain unchanged; also add a separate test using
renderSheet({ hasRacks: false }) to assert the "View YAML" button is disabled
and clicking it does not call onViewYAML or onClose. Ensure you reference the
existing helpers and mocks (renderSheet, onViewYAML/onViewYaml, onClose, onLoad,
onSave, onExport, onShare) when adding assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/components/MobileFileSheet.svelte`:
- Around line 97-103: The View YAML button currently calls
handleAction(onviewyaml) which triggers onviewyaml then the generic onclose
path, causing the YAML sheet to open then immediately close; fix by ensuring the
View YAML action does not invoke the generic close: either (preferred) change
the button to call onviewyaml directly from MobileFileSheet.svelte (bypassing
handleAction) or update handleAction to accept a skipClose flag (e.g.,
handleAction(action, { skipClose: true })) and, when that flag is set or when
action === onviewyaml, avoid calling onclose; update references to handleAction,
onviewyaml and onclose accordingly.

In `@src/tests/LayoutYamlPanel.test.ts`:
- Around line 33-37: Replace direct DOM access of the textarea value in the
tests with testing-library matchers: locate the element via
screen.getByTestId("yaml-textarea") and use the jest-dom matcher to assert its
value (e.g., toHaveValue with expect.stringContaining("name: Baseline Layout"))
inside the existing waitFor. Apply the same change to the other two occurrences
(the assertions referenced around lines 75-79 and 126-130) so tests use
toHaveValue/assertions from jest-dom instead of casting and reading .value
directly.

---

Outside diff comments:
In `@src/tests/MobileFileSheet.test.ts`:
- Around line 87-96: Add user-facing tests for the new "View YAML" action: after
calling renderSheet({ hasRacks: true }), assert the "View YAML" button is
enabled (getByRole("button", { name: "View YAML" })), simulate a click with
fireEvent.click and assert the onViewYAML (or onViewYaml) callback is called
once and onClose is called once while onLoad/onSave/onExport/onShare remain
unchanged; also add a separate test using renderSheet({ hasRacks: false }) to
assert the "View YAML" button is disabled and clicking it does not call
onViewYAML or onClose. Ensure you reference the existing helpers and mocks
(renderSheet, onViewYAML/onViewYaml, onClose, onLoad, onSave, onExport, onShare)
when adding assertions.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6252184 and 7331eca.

📒 Files selected for processing (4)
  • src/App.svelte
  • src/lib/components/MobileFileSheet.svelte
  • src/tests/LayoutYamlPanel.test.ts
  • src/tests/MobileFileSheet.test.ts

Comment on lines 97 to 103
<button
type="button"
class="file-action"
disabled={!hasRacks}
onclick={() => handleAction(onviewyaml)}
aria-label="View YAML"
>
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

View YAML can close the YAML sheet immediately on mobile.

handleAction runs onviewyaml before onclose; in the mobile path that can open the YAML sheet and then immediately close it via the generic close callback.

🐛 Localized fix for `View YAML` handoff
   <button
     type="button"
     class="file-action"
     disabled={!hasRacks}
-    onclick={() => handleAction(onviewyaml)}
+    onclick={() => {
+      onclose?.();
+      onviewyaml?.();
+    }}
     aria-label="View YAML"
   >
📝 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
<button
type="button"
class="file-action"
disabled={!hasRacks}
onclick={() => handleAction(onviewyaml)}
aria-label="View YAML"
>
<button
type="button"
class="file-action"
disabled={!hasRacks}
onclick={() => {
onclose?.();
onviewyaml?.();
}}
aria-label="View YAML"
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/components/MobileFileSheet.svelte` around lines 97 - 103, The View
YAML button currently calls handleAction(onviewyaml) which triggers onviewyaml
then the generic onclose path, causing the YAML sheet to open then immediately
close; fix by ensuring the View YAML action does not invoke the generic close:
either (preferred) change the button to call onviewyaml directly from
MobileFileSheet.svelte (bypassing handleAction) or update handleAction to accept
a skipClose flag (e.g., handleAction(action, { skipClose: true })) and, when
that flag is set or when action === onviewyaml, avoid calling onclose; update
references to handleAction, onviewyaml and onclose accordingly.

Comment on lines 33 to 37
await waitFor(() => {
expect(
(screen.getByTestId("yaml-textarea") as HTMLTextAreaElement).value,
).toContain("name: Baseline Layout");
});
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

Avoid direct DOM node .value access in tests.

These assertions should use testing-library matchers instead of casting and reading textarea .value directly.

♻️ Suggested pattern (apply to all three occurrences)
 await waitFor(() => {
-  expect(
-    (screen.getByTestId("yaml-textarea") as HTMLTextAreaElement).value,
-  ).toContain("name: Baseline Layout");
+  expect(screen.getByTestId("yaml-textarea")).toHaveValue(
+    expect.stringContaining("name: Baseline Layout"),
+  );
 });

As per coding guidelines, “ESLint hard-blocks … DOM node access in tests …”.

Also applies to: 75-79, 126-130

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

In `@src/tests/LayoutYamlPanel.test.ts` around lines 33 - 37, Replace direct DOM
access of the textarea value in the tests with testing-library matchers: locate
the element via screen.getByTestId("yaml-textarea") and use the jest-dom matcher
to assert its value (e.g., toHaveValue with expect.stringContaining("name:
Baseline Layout")) inside the existing waitFor. Apply the same change to the
other two occurrences (the assertions referenced around lines 75-79 and 126-130)
so tests use toHaveValue/assertions from jest-dom instead of casting and reading
.value directly.

…heet

Cover the new hasRacks-gated behaviour: View YAML calls handler when
enabled, Share and View YAML buttons are disabled when no racks exist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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: 1

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

Inline comments:
In `@src/tests/MobileFileSheet.test.ts`:
- Around line 20-29: The test helper renderSheet currently types its overrides
as Record<string, unknown>, which is too permissive; change the overrides
parameter to use Partial<ComponentProps<typeof MobileFileSheet>> so TypeScript
will validate prop names and types against the component; import ComponentProps
from 'react' (or React) if not already imported and update the renderSheet
signature and any related usages to use the new typed overrides while preserving
the spreading of onload/onsave/... into props.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7331eca and 293559d.

📒 Files selected for processing (1)
  • src/tests/MobileFileSheet.test.ts

Comment on lines 20 to 29
function renderSheet(overrides: Record<string, unknown> = {}) {
return render(MobileFileSheet, {
props: {
onload: onLoad,
onsave: onSave,
onexport: onExport,
onshare: onShare,
onclose: onClose,
...overrides,
},
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:

find . -name "MobileFileSheet.test.ts" -o -name "MobileFileSheet.svelte" | head -20

Repository: RackulaLives/Rackula

Length of output: 143


🏁 Script executed:

fd -t f "MobileFileSheet"

Repository: RackulaLives/Rackula

Length of output: 139


🏁 Script executed:

cat -n ./src/tests/MobileFileSheet.test.ts | head -120

Repository: RackulaLives/Rackula

Length of output: 4408


🏁 Script executed:

cat -n ./src/lib/components/MobileFileSheet.svelte

Repository: RackulaLives/Rackula

Length of output: 5194


🏁 Script executed:

rg "ComponentProps" --type ts --type tsx | head -20

Repository: RackulaLives/Rackula

Length of output: 91


🏁 Script executed:

cat -n ./tsconfig.json | head -30

Repository: RackulaLives/Rackula

Length of output: 981


🏁 Script executed:

rg "ComponentProps" --type ts | head -20

Repository: RackulaLives/Rackula

Length of output: 46


🏁 Script executed:

rg "from.*svelte" --type ts | grep -i import | head -15

Repository: RackulaLives/Rackula

Length of output: 1324


🏁 Script executed:

cat package.json | grep -A 5 '"svelte"'

Repository: RackulaLives/Rackula

Length of output: 226


🏁 Script executed:

rg "ComponentProps" . --type ts

Repository: RackulaLives/Rackula

Length of output: 46


🌐 Web query:

Svelte 5 ComponentProps type extract component props from .svelte file

💡 Result:

Use Svelte’s built-in ComponentProps utility type with the component’s type (typeof), e.g.:

import Comp from './Comp.svelte';
import type { ComponentProps } from 'svelte';

type CompProps = ComponentProps<typeof Comp>;

If you want to avoid a runtime import, you can do it purely as a type:

import type { ComponentProps } from 'svelte';

type CompProps = ComponentProps<typeof import('./Comp.svelte').default>;

This works in Svelte 5 and gives you the props type inferred from the .svelte component. [1][2]

Sources: [1] Stack Overflow answer showing ComponentProps<typeof Comp> usage, [2] Svelte’s published type definitions include ComponentProps.


🏁 Script executed:

rg "type.*ComponentProps" src/tests/ --type ts

Repository: RackulaLives/Rackula

Length of output: 46


🏁 Script executed:

cat -n ./src/tests/MobileFileSheet.test.ts | grep -A 5 -B 5 "renderSheet"

Repository: RackulaLives/Rackula

Length of output: 3229


Tighten renderSheet override typing to component props

Record<string, unknown> is too loose for strict TypeScript and won't catch invalid prop keys in tests. Use Partial<ComponentProps<typeof MobileFileSheet>> instead.

Proposed diff
+import type { ComponentProps } from "svelte";
 import { describe, it, expect, vi, beforeEach } from "vitest";
 import { render, screen, fireEvent } from "@testing-library/svelte";
 import MobileFileSheet from "$lib/components/MobileFileSheet.svelte";
@@
-  function renderSheet(overrides: Record<string, unknown> = {}) {
+  function renderSheet(
+    overrides: Partial<ComponentProps<typeof MobileFileSheet>> = {},
+  ) {
     return render(MobileFileSheet, {
       props: {
         onload: onLoad,
         onsave: onSave,
         onexport: onExport,
         onshare: onShare,
         onclose: onClose,
         ...overrides,
       },
     });
   }

The behavioral tests validating user-facing actions, conditional disabled states, and handler callbacks are well-designed and comply with testing guidelines.

📝 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 renderSheet(overrides: Record<string, unknown> = {}) {
return render(MobileFileSheet, {
props: {
onload: onLoad,
onsave: onSave,
onexport: onExport,
onshare: onShare,
onclose: onClose,
...overrides,
},
function renderSheet(
overrides: Partial<ComponentProps<typeof MobileFileSheet>> = {},
) {
return render(MobileFileSheet, {
props: {
onload: onLoad,
onsave: onSave,
onexport: onExport,
onshare: onShare,
onclose: onClose,
...overrides,
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tests/MobileFileSheet.test.ts` around lines 20 - 29, The test helper
renderSheet currently types its overrides as Record<string, unknown>, which is
too permissive; change the overrides parameter to use
Partial<ComponentProps<typeof MobileFileSheet>> so TypeScript will validate prop
names and types against the component; import ComponentProps from 'react' (or
React) if not already imported and update the renderSheet signature and any
related usages to use the new typed overrides while preserving the spreading of
onload/onsave/... into props.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 26, 2026

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai bot added size:XL This PR changes 500-999 lines, ignoring generated files and removed size:XL This PR changes 500-999 lines, ignoring generated files labels Feb 26, 2026
Comment on lines 126 to 145
async function syncFromLayout(
sourceLayout: Layout,
options: { allowWhileEditing: boolean } = { allowWhileEditing: true },
): Promise<void> {
const runId = ++syncRun;
debug("syncing from layout, runId=%d", runId);
const editingAtStart = isEditing;
const serialized = await serializeLayoutToYaml(sourceLayout);
if (runId !== syncRun) return;
if (!options.allowWhileEditing && editingAtStart) return;

baselineYaml = serialized;
yamlText = serialized;
syntaxError = null;
schemaError = null;
isValidating = false;
showConflictPrompt = false;
latestYamlAtConflict = null;
pendingLayout = null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: The use of editingAtStart in syncFromLayout only checks whether editing was active when the sync started, so if the user switches into edit mode while serializeLayoutToYaml is still in flight, the late-arriving sync will still overwrite yamlText and baselineYaml with the serialized layout, clobbering any edits just made; instead, the post-await guard should check the current isEditing state so in-flight syncs are discarded once editing has begun. [race condition]

Severity Level: Major ⚠️
- ❌ Initial YAML edits can be silently discarded on open.
- ⚠️ Users may distrust Layout YAML editor reliability.
- ⚠️ Race affects File menu "View YAML" workflow.
Suggested change
async function syncFromLayout(
sourceLayout: Layout,
options: { allowWhileEditing: boolean } = { allowWhileEditing: true },
): Promise<void> {
const runId = ++syncRun;
debug("syncing from layout, runId=%d", runId);
const editingAtStart = isEditing;
const serialized = await serializeLayoutToYaml(sourceLayout);
if (runId !== syncRun) return;
if (!options.allowWhileEditing && editingAtStart) return;
baselineYaml = serialized;
yamlText = serialized;
syntaxError = null;
schemaError = null;
isValidating = false;
showConflictPrompt = false;
latestYamlAtConflict = null;
pendingLayout = null;
}
async function syncFromLayout(
sourceLayout: Layout,
options: { allowWhileEditing: boolean } = { allowWhileEditing: true },
): Promise<void> {
const runId = ++syncRun;
debug("syncing from layout, runId=%d", runId);
const serialized = await serializeLayoutToYaml(sourceLayout);
if (runId !== syncRun) return;
if (!options.allowWhileEditing && isEditing) return;
baselineYaml = serialized;
yamlText = serialized;
syntaxError = null;
schemaError = null;
isValidating = false;
showConflictPrompt = false;
latestYamlAtConflict = null;
pendingLayout = null;
}
Steps of Reproduction ✅
1. Open the app and trigger the Layout YAML panel by clicking "View YAML" from the File
menu (`src/lib/components/FileMenu.svelte:98` label) which shows `<LayoutYamlPanel>` in
`src/App.svelte:1795`.

2. When the panel opens, the `$effect` at
`src/lib/components/LayoutYamlPanel.svelte:65-68` runs with `open === true`, `isEditing
=== false`, `isApplying === false`, and calls `syncFromLayout(layout, { allowWhileEditing:
false })` to load YAML asynchronously.

3. Before `serializeLayoutToYaml(layout)` in `syncFromLayout` resolves
(`LayoutYamlPanel.svelte:133`), click "Edit YAML" (button at
`LayoutYamlPanel.svelte:305-311`), which sets `isEditing = true` via
`handleEditModeToggle()` (`LayoutYamlPanel.svelte:189-197`), then immediately type some
changes into the textarea bound to `yamlText` (`LayoutYamlPanel.svelte:351-358`).

4. When the in-flight `serializeLayoutToYaml` promise resolves, `syncRun` still matches
`runId` (`LayoutYamlPanel.svelte:130-135`) and `editingAtStart` is still `false`, so the
guard `if (!options.allowWhileEditing && editingAtStart) return;` does not fire; as a
result, `baselineYaml` and `yamlText` are overwritten with `serialized`
(`LayoutYamlPanel.svelte:137-138`), silently discarding the user edits just made while
`isEditing` is now `true`. Changing the guard to check `isEditing` (current state)
prevents this overwrite once editing has begun.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/lib/components/LayoutYamlPanel.svelte
**Line:** 126:145
**Comment:**
	*Race Condition: The use of `editingAtStart` in `syncFromLayout` only checks whether editing was active when the sync started, so if the user switches into edit mode while `serializeLayoutToYaml` is still in flight, the late-arriving sync will still overwrite `yamlText` and `baselineYaml` with the serialized layout, clobbering any edits just made; instead, the post-`await` guard should check the current `isEditing` state so in-flight syncs are discarded once editing has begun.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Feb 26, 2026

CodeAnt AI Incremental review completed.

@ggfevans ggfevans merged commit dd25f4c into main Feb 26, 2026
8 checks passed
@ggfevans ggfevans deleted the feat/1323-yaml-editor-apply-flow branch February 26, 2026 10:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extract YAML editor feature from dev/yamllama-ding-dong

1 participant