Skip to content

refactor: migrate to Svelte 5#1248

Merged
chhoumann merged 15 commits into
masterfrom
feat/svelte5-rewrite
May 29, 2026
Merged

refactor: migrate to Svelte 5#1248
chhoumann merged 15 commits into
masterfrom
feat/svelte5-rewrite

Conversation

@chhoumann
Copy link
Copy Markdown
Owner

@chhoumann chhoumann commented May 29, 2026

Closes #1239.

Full, idiomatic Svelte 4 → 5 rewrite of the plugin UI — not a mechanical sv migrate port. All 19 components are converted to runes, every imperative host uses mount()/unmount(), and behavior/UX is preserved (no UI redesign). Landed as ordered, individually-green commits so it can be reviewed slice by slice.

What changed

Slice Commit Scope
1 tooling svelte→5, svelte-check→4, @sveltejs/vite-plugin-svelte@^6 (vite 7), @testing-library/svelte, eslint-plugin-svelte; vitest svelte pipeline + obsidian-stub setIcon
2 shared utils mountComponent() (mount/unmount + idempotent handle), dndReorder (stripShadow/replaceById) + pure tests
3 ObsidianIcon $props + a single $effect for the setIcon DOM write
4 macro cluster CommandList + 7 command children + CommandSequenceEditor host; deleted the updateCommandList bridge → host-owned $state props bag
5 choiceList cluster 6 recursive components; one shared ChoiceListActions callback bag; deleted the dead bind:multiChoice; removed as any recursion casts
6 remaining hosts GlobalVariablesView, both PackageManager modals, FolderList + their .ts hosts on mountComponent; fixed a ChoiceBuilder.reload() effect leak
7 cleanup wired eslint-plugin-svelte; fixed {#each} keys / mustaches; final residue sweep
conditional fix persist Conditional Then/Else branch edits (see below)
e2e test real-GUI regression test for the above

Key architecture decisions

  • State: zustand (settingsStore.ts) stays the single source of truth (it's consumed by ~30 non-Svelte files + test mocks). Components read it via a thin runes boundary; $state.snapshot() is applied at every reactive→plain persistence boundary so no proxy leaks into data.json.
  • Events: all 26 createEventDispatcher + 147 on: directives → on-prefixed callback props with flattened payloads. The recursive choiceList threads one shared ChoiceListActions bag.
  • Imperative hosts: one mountComponent() helper replaces new Component()/.$destroy()/.$on() everywhere; the two hidden exported-function bridges (updateCommandList, updateFolders) are gone in favor of host-owned $state.
  • DnD (load-bearing, fix(macros): commands no longer disappear when reordering in the macro editor #1244/fix(gui): eliminate ghost gap after drag in Settings Choice list (#882) #883): the SHADOW_PLACEHOLDER logic is extracted once into a tested dndReorder.ts; ChoiceList's unconditional finalize re-disable is preserved (distinct from CommandList's POINTER-gated one).

The one regression found in QA — fixed

Adding commands to a Conditional command's Then/Else branch didn't persist. Root cause: a Svelte 5 rule — $state proxy mutations don't write through to the original object. Every other edit persisted via saveCommands($state.snapshot(commands)) (reads the proxy's live state), but the conditional handlers uniquely persisted via the host's stale commandsRef. Fix: route them through the same snapshot path. (Also fixed a latent identical bug in configure-condition.)

Verification

  • svelte-check (4): 0 errors / 0 warnings
  • build-with-lint (tsc + eslint incl. svelte + esbuild): green
  • unit/integration tests: 1429 passed (baseline 1400; +29 new, incl. component tests via @testing-library/svelte)
  • Real-GUI e2e (obsidian-e2e, tests/e2e/conditional-branch-persistence.test.ts): drives the live macro editor (configure → Edit then branch → Add wait → Save → close) and asserts the command lands in data.json. Passes.

Manual dev-vault QA (maintainer): passed — flat + nested choice drag-reorder (no vanish/ghost-gap, persists on reload), every macro command type incl. drag-reorder, WaitCommand time edit, conditional branches (after fix), choice CRUD, packages, folder add/remove, settings open/close (no mount leak), no Proxy artifacts in data.json, no console errors.

Notes for reviewers

  • Title is refactor: (no semantic-release version bump) since this is behavior-preserving modernization — switch to feat: if you'd prefer a minor release.
  • 0 createEventDispatcher / svelte/legacy / SvelteComponent / componentApi compat remain in code.
  • Best read commit-by-commit (each is independently green: check + build + test).

Open in Devin Review

Summary by CodeRabbit

  • Chores
    • Upgraded to Svelte 5, refreshed dev tooling and linting for modern build/test flows.
  • New Features
    • Improved mounting for more reliable modals, dialogs, and embedded UI widgets.
    • Added helpers for safe snapshot-based persistence and imperative component props.
  • Bug Fixes
    • More robust drag‑and‑drop reordering and stable command/branch persistence.
    • Consistent UI event handling for buttons, inputs, and keyboard interactions.
  • Tests
    • Expanded unit, integration, smoke, and end‑to‑end test coverage for UI flows.

Review Change Stack

chhoumann added 9 commits May 29, 2026 15:19
…ponent-test infra

- svelte ^4.2.20 -> ^5; svelte-check ^3.8.6 -> ^4
- add @sveltejs/vite-plugin-svelte ^6 (vite 7 compat; NOT ^5/^7), @testing-library/svelte+jest-dom+user-event, eslint-plugin-svelte + svelte-eslint-parser
- tsconfig: skipLibCheck:true so svelte-check matches the build's tsc -skipLibCheck (third-party .d.ts only; our source still fully checked)
- vitest: svelte() + svelteTesting() plugins + jest-dom setup
- obsidian-stub: add module-level setIcon (emits <svg data-icon>) + prepareFuzzySearch
- smoke gate: ObsidianIcon renders via the new pipeline; svelte-dnd-action exports resolve

All components remain legacy (Svelte 4 syntax) and compile under Svelte 5 legacy mode.
- src/gui/svelte/mountComponent.ts: mount()/unmount() wrapper with an idempotent
  typed MountHandle (replaces new Component()/.$destroy() across hosts)
- src/gui/shared/dndReorder.ts: stripShadow + replaceById pure helpers (one tested
  home for the SHADOW_PLACEHOLDER filtering + the immutable item replace)
- pure tests: dndReorder (8) + mountComponent mount/teardown/idempotency (3)

choiceListActions.ts deferred to slice 5 (signatures depend on the cluster rewrite).
- export let -> $props(); onMount + reactive $: -> a single $effect (setIcon is a
  DOM side-effect, so $effect not $derived); iconEl as $state for bind:this
- ObsidianIcon.test.ts: mount render, size default, reactive icon swap, clean unmount

Rescope vs blueprint: only ObsidianIcon is a truly standalone leaf (no events, props
passed one-way), so it converts safely while parents stay legacy. FolderList moves to
the templateChoiceBuilder host slice (updateFolders bridge); ChoiceItemRightButtons
moves to the choiceList cluster slice (consumed via component events).
…o runes

Components (createEventDispatcher/on: -> callback props + on*; bind:-read props -> one-way):
- CommandList: commands=$bindable $state; onconsider/onfinalize via shared stripShadow;
  updateCommand via replaceById; $state.snapshot at the saveCommands boundary;
  DELETE exported updateCommandList bridge + createEventDispatcher
- 7 command children: $props + onDeleteCommand/onConfigure* callbacks; ConditionalCommand
  $: -> $derived; WaitCommand local $state(untrack) time mirror + onUpdateCommand (explicit
  persistence, replacing shared-ref mutation)
Host (CommandSequenceEditor.ts):
- new CommandList()/.$destroy()/.$on() -> mountComponent + createCommandListProps ($state
  props bag) + callback props; addCommand/emitCommandsChanged now immutable (callers track
  via onCommandsChange, verified in MacroBuilder + ConditionalBranchEditorModal)
Shared: commandListProps.svelte.ts ($state bag replaces the updateCommandList bridge)
Tests: macroCommands (delete/time-update/conditional routing) + CommandList (no-vanish +
host-push bridge) = 5; obsidian-dataview mocked per repo convention.

Verified: svelte-check 0 errors, build green, full suite 1423 pass.
PENDING MANUAL QA (jsdom can't drive pointer drag): actual command drag-reorder in the dev
vault (#1244 guard) — tracked for final QA.
All 6 components (ChoiceView, ChoiceList, ChoiceListItem, MultiChoiceListItem,
ChoiceItemRightButtons, AddChoiceBox):
- createEventDispatcher + bare on: forwarding -> one shared ChoiceListActions callback
  bag (choiceListActions.ts), threaded by reference through the recursion (flattened
  payloads); MultiChoiceListItem builds nestedActions overriding onReorderChoices to
  write choice.choices then bubble [...roots] (calls parent's onReorderChoices, no loop)
- ChoiceView: $bindable $state choices buffer; one-shot settingsStore subscribe in
  $effect; $state.snapshot at every saveChoices boundary; AI-button getState read kept
  non-reactive (behavior preserved); on:->on* ; flattened handler signatures
- ChoiceList: choices=$bindable $state; stripShadow at consider/finalize/each; UNCONDITIONAL
  finalize re-disable preserved (NOT the macro POINTER gate); forceDragDisabled early-return
- DELETED the dead bind:multiChoice landmine; REMOVED the as-any recursion casts (cycle now
  type-resolves); $: -> $effect for renderChoiceName; fixed self-closing span
- host: quickAddSettingsTab mounts ChoiceView via mountComponent (GlobalVariablesView stays
  legacy new() until slice 6)
Tests: ChoiceList.callbacks (flat + nested recursion wiring) + persistenceBoundary (snapshot
is plain + non-aliasing). Full ChoiceView render not unit-testable (pre-existing circular
imports in its graph — bundler-only); covered by manual QA.

Verified: svelte-check 0 errors/0 warnings, build green, full suite 1427 pass.
PENDING MANUAL QA: nested drag reorder, collapse toggle, choice CRUD + data.json persistence.
Components -> runes:
- GlobalVariablesView: $props + items $state; onMount/onDestroy -> $effect with cleanup;
  preserved bind:value={it.name}/{it.value} + use:attachSuggester (source-guarded by test)
- ExportPackageModal: $props + $state; 5 pure $: -> $derived (incl. let+$: rootChoiceIds merge)
- ImportPackageModal: $props + $state (incl. reactive Maps); preserved {conflict.name}/{originalPath}
- FolderList: $props; DELETED exported updateFolders bridge -> folderListProps.svelte.ts $state bag
Hosts -> mountComponent + handle.destroy():
- ExportPackageModal.ts / ImportPackageModal.ts (onOpen/onClose)
- templateChoiceBuilder.addFolderSelector (folderListProps $state bag replaces updateFolders)
- choiceBuilder base: svelteElements SvelteComponent[] -> MountHandle[]; destroySvelteElements()
  in onClose AND at top of reload() (fixes the verified effect leak)
- quickAddSettingsTab: GlobalVariablesView via mountComponent

All 19 .svelte are runes; 0 export let / createEventDispatcher / on: directives remain;
remaining new X({...}) are plain TS classes (CommandSequenceEditor/ConditionalBranchEditorModal/
InputPromptDraftHandler), not Svelte.

Verified: svelte-check 0/0, build green, full suite 1427 pass.
PENDING MANUAL QA: package import/export, global vars inline edit + debounce, folder add/remove +
builder reload re-render, settings open/close mount-leak.
…eanup

- eslint.config.mjs: add svelte flat/recommended + svelte-eslint-parser (TS sub-parser)
  for **/*.svelte + **/*.svelte.ts; disable prefer-svelte-reactivity (collections use
  immutable reassignment, which is reactive — SvelteSet/Map only needed for mutation)
- fix all surfaced lint: {#each} keys (FolderList/GlobalVariables/Export/Import),
  useless string mustaches in AddChoiceBox (auto-fixed), drop unused suggester var +
  dangling eslint-disable directive in GlobalVariablesView

Final state: 19/19 components on runes ($props); 0 createEventDispatcher / svelte/legacy /
SvelteComponent / componentApi compat in code; all hosts use mountComponent.

Full CI gate set GREEN: svelte-check 4 (0 errors/0 warnings), build-with-lint (tsc + eslint
incl. svelte + esbuild), test (1427 passed / 31 skipped).
…condition)

QA surfaced that adding commands to a Conditional command's Then/Else branch didn't
persist. Root cause: the host's commandListProps is $state, so the rendered command is
a PROXY; the branch modal mutates command.thenCommands on that proxy, which does NOT
write through to the host's commandsRef (Svelte 5: 'updating proxy properties does not
mutate the original'). Every other edit persists via saveCommands($state.snapshot(...)),
which reads the proxy's current state — but the conditional handlers uniquely persisted
via emitCommandsChanged(commandsRef), passing the stale, un-mutated array.

Fix: route the conditional handlers through the same snapshot path. CommandList now
awaits the handler (which mutates + returns whether changed) and calls updateCommand ->
saveCommands($state.snapshot(commands)). Dropped the host's stale wrapConditionalHandler/
emitCommandsChanged wiring; handlers passed directly. Also fixes the latent
configure-condition persistence (same bug).

Regression test: CommandList.conditional.test.ts (proxy mutation -> plain snapshot save).
Verified: svelte-check 0/0, build-with-lint, full suite 1429 pass.
…stence

Drives the live macro editor through obsidian-e2e (configure macro -> Edit then
branch -> Add wait command -> Save -> close) and asserts the added command lands in
data.json (thenCommands.length === 1). Guards the runes-rewrite fix end-to-end.

Verified PASSING against the live dev vault; complements the component-level guard in
CommandList.conditional.test.ts (which fails on the pre-fix code).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2f3dc217-3ffe-4b49-8ff1-917a6236411f

📥 Commits

Reviewing files that changed from the base of the PR and between 8d61ac8 and 7773f15.

📒 Files selected for processing (1)
  • .gitignore
✅ Files skipped from review due to trivial changes (1)
  • .gitignore

📝 Walkthrough

Walkthrough

Migrates UI to Svelte 5 runes ($props/$state/$effect), replaces component event dispatch with typed callback-prop contracts, adds mountComponent/MountHandle for imperative mounting, introduces immutable DnD and snapshot helpers, upgrades Svelte tooling, and expands unit, smoke, and E2E tests.

Changes

Svelte 5 Migration & Callback-Props Refactor

Layer / File(s) Summary
Tooling & Test Configuration
eslint.config.mjs, package.json, vitest.config.mts, svelte.config.mjs, tests/vitest-setup.ts, tests/obsidian-stub.ts
Adds Svelte ESLint parser/plugin and Svelte/Vite test plugins, bumps Svelte/dev deps to v5, provides svelte.config.mjs, and extends test stubs and setup.
Imperative mount utility & tests
src/gui/svelte/mountComponent.ts, src/gui/svelte/mountComponent.test.ts
Adds MountHandle and mountComponent wrapper around Svelte mount/unmount with idempotent destroy and lifecycle tests.
Shared props/contracts & persist snapshot
src/gui/MacroGUIs/commandListProps.svelte.ts, src/gui/ChoiceBuilder/folderListProps.svelte.ts, src/gui/choiceList/choiceListActions.ts, src/gui/svelte/persist.svelte.ts
Defines CommandListProps and FolderListProps factories, ChoiceListActions interface, and Plain/snapshot() persistence helper.
DND helpers & tests
src/gui/shared/dndReorder.ts, src/gui/shared/dndReorder.test.ts
Adds immutable stripShadow and replaceById helpers with unit tests ensuring immutability and correct behavior.
Choice list components & buffering
src/gui/choiceList/*, src/gui/choiceList/persistenceBoundary.svelte.ts, tests under src/gui/choiceList/*
Migrates ChoiceList, ChoiceListItem, MultiChoiceListItem, RightButtons, AddChoiceBox to $props/$state/$effect and ChoiceListActions callbacks; adds persistence buffer factory and tests for callback wiring and lifecycle unload.
Macro command list & components
src/gui/MacroGUIs/CommandList.svelte, src/gui/MacroGUIs/Components/*, src/gui/MacroGUIs/commandListProps.svelte.ts, tests
Migrates CommandList and subcomponents to callback props via CommandListProps, replaces in-place mutation with replaceById + snapshot persistence, and adds tests for reorder and conditional-branch persistence.
CommandSequenceEditor mounting & immutability
src/gui/MacroGUIs/CommandSequenceEditor.ts
Uses createCommandListProps + mountComponent, stores handle/props, updates commands immutably and propagates changes to mounted props.
Export/Import modals & settings integration
src/gui/PackageManager/*, src/quickAddSettingsTab.ts
Migrates modals to runes and mounts via mountComponent; updates QuickAddSettingsTab to use MountHandle fields and robust cleanup.
Global variables view & icon component
src/gui/GlobalVariables/GlobalVariablesView.svelte, src/gui/components/ObsidianIcon.svelte, tests
Migrates GlobalVariablesView to runes with $effect subscription cleanup; refactors ObsidianIcon to $effect-driven DOM updates and adds tests and smoke gate.
E2E & smoke tests
tests/e2e/conditional-branch-persistence.test.ts, src/gui/components/svelte5-pipeline.smoke.test.ts
Adds E2E regression test for conditional-branch persistence and a Svelte 5 smoke test exercising the Vite/Svelte test pipeline.
Config tweaks
tsconfig.json
Reorders compilerOptions entries (skipLibCheck, noImplicitAny, moduleResolution) with no behavioral changes.

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues:

Possibly related PRs:

Suggested labels:
released

"I hopped through props and state,
Swapped dispatch for callbacks great.
Mounted components, tests that sing —
Svelte runes and tidy spring.
🐇✨"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor: migrate to Svelte 5' clearly and concisely summarizes the main change: upgrading from Svelte 4 to Svelte 5 with a complete component rewrite using runes.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 feat/svelte5-rewrite

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

❤️ Share

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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3bd4a5aa13

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread package.json
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 29, 2026

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7773f15
Status: ✅  Deploy successful!
Preview URL: https://7763a301.quickadd.pages.dev
Branch Preview URL: https://feat-svelte5-rewrite.quickadd.pages.dev

View logs

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread src/gui/ChoiceBuilder/FolderList.svelte
Copy link
Copy Markdown

@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

🧹 Nitpick comments (3)
src/gui/components/ObsidianIcon.svelte (1)

2-20: ⚡ Quick win

Use tabs instead of spaces in this Svelte file block.

This section is indented with spaces, but this path is configured for tab indentation; please reformat to tabs to match repo/editorconfig expectations.

As per coding guidelines: "src/**/*.{ts,tsx,svelte,css,js}: Use tab indentation and LF endings (see .editorconfig); align editor settings".

🤖 Prompt for 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.

In `@src/gui/components/ObsidianIcon.svelte` around lines 2 - 20, Reformat this
Svelte component to use tabs for indentation (instead of spaces) to comply with
the repo .editorconfig: replace leading space indentation in the file (including
the import line and the block that defines iconId, size, iconEl, and the
$effect) with tabs, preserving existing code and line endings (LF); ensure the
blocks referencing setIcon, iconEl, iconId, size, and the $effect remain
unchanged except for indentation so the component behavior is identical after
reformatting.
src/gui/MacroGUIs/Components/AIAssistantCommand.svelte (1)

5-17: ⚡ Quick win

Consider a shared base type for the repeated child-prop contract.

startDrag, dragDisabled, and onDeleteCommand are re-declared identically across all seven command-child components. Extracting a small shared interface keeps the contract from drifting if any signature changes.

♻️ Example shared base type
// e.g. src/gui/MacroGUIs/commandChildProps.ts
import type { ICommand } from "../../types/macros/ICommand";

export interface CommandChildProps<T extends ICommand = ICommand> {
	command: T;
	startDrag: (e: MouseEvent | TouchEvent) => void;
	dragDisabled: boolean;
	onDeleteCommand: (commandId: string) => void;
}
-	}: {
-		command: IAIAssistantCommand;
-		startDrag: (e: MouseEvent | TouchEvent) => void;
-		dragDisabled: boolean;
-		onDeleteCommand: (commandId: string) => void;
-		onConfigureAssistant: (command: IAIAssistantCommand) => void;
-	} = $props();
+	}: CommandChildProps<IAIAssistantCommand> & {
+		onConfigureAssistant: (command: IAIAssistantCommand) => void;
+	} = $props();
🤖 Prompt for 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.

In `@src/gui/MacroGUIs/Components/AIAssistantCommand.svelte` around lines 5 - 17,
Extract the repeated child-prop contract into a shared interface and use it in
AIAssistantCommand.svelte: create a new interface (e.g., CommandChildProps) that
includes startDrag, dragDisabled, and onDeleteCommand, then update the
component's prop typing to extend or use CommandChildProps with
IAIAssistantCommand (replace the inline type block currently declaring command,
startDrag, dragDisabled, onDeleteCommand in AIAssistantCommand.svelte); ensure
other sibling command-child components are updated to import and use the same
CommandChildProps so the signatures (startDrag, dragDisabled, onDeleteCommand)
remain consistent across all seven components.
src/gui/ChoiceBuilder/templateChoiceBuilder.ts (1)

277-291: 💤 Low value

Optional: align mutation style for choice.folder.folders.

addFolder mutates the backing array in place via push(), while deleteFolder (Line 246) reassigns a new filtered array. Both paths then re-sync folderListProps.folders and the suggester, so behavior is correct — this is purely for consistency with the immutable reassignment pattern used elsewhere in the migration.

♻️ Optional: reassign instead of push
-		this.choice.folder.folders.push(input);
-
-		folderListProps.folders = [...this.choice.folder.folders];
+		this.choice.folder.folders = [...this.choice.folder.folders, input];
+
+		folderListProps.folders = [...this.choice.folder.folders];
🤖 Prompt for 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.

In `@src/gui/ChoiceBuilder/templateChoiceBuilder.ts` around lines 277 - 291,
addFolder currently mutates the backing array with
this.choice.folder.folders.push(input) while deleteFolder reassigns a filtered
array; make addFolder consistent by creating a new array and assigning it to
this.choice.folder.folders (e.g., this.choice.folder.folders =
[...this.choice.folder.folders, input]), then update folderListProps.folders and
call suggester.updateCurrentItems as before; modify the addFolder function to
perform immutable reassignment while keeping folderListProps.folders and
folderInput.inputEl.value updates intact.
🤖 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 `@src/gui/choiceList/MultiChoiceListItem.svelte`:
- Around line 33-39: The Component instance cmp created in
MultiChoiceListItem.svelte is not unloaded on destroy; ensure you either
register an onDestroy cleanup to call cmp.unload() (e.g., onDestroy(() =>
cmp.unload())) or verify and document that renderChoiceName(choice.name,
nameElement, cmp, app) does not attach resources needing disposal; add the
onDestroy hook if prior code in ChoiceListItem used cmp.unload(), and keep the
$effect block using nameElement and renderChoiceName unchanged.

In `@src/gui/components/svelte5-pipeline.smoke.test.ts`:
- Line 40: Update the stale comment that references the removed legacy flow
"onMount -> updateIcon" to accurately describe the current runtime path (e.g.,
"$effect -> setIcon(el, 'trash')" and that the stub appends "<svg data-icon>").
Locate the comment near the test in svelte5-pipeline.smoke.test.ts and replace
the outdated sequence with a short note describing the actual "$effect ->
setIcon(...)" behavior so future readers understand the current flow.

---

Nitpick comments:
In `@src/gui/ChoiceBuilder/templateChoiceBuilder.ts`:
- Around line 277-291: addFolder currently mutates the backing array with
this.choice.folder.folders.push(input) while deleteFolder reassigns a filtered
array; make addFolder consistent by creating a new array and assigning it to
this.choice.folder.folders (e.g., this.choice.folder.folders =
[...this.choice.folder.folders, input]), then update folderListProps.folders and
call suggester.updateCurrentItems as before; modify the addFolder function to
perform immutable reassignment while keeping folderListProps.folders and
folderInput.inputEl.value updates intact.

In `@src/gui/components/ObsidianIcon.svelte`:
- Around line 2-20: Reformat this Svelte component to use tabs for indentation
(instead of spaces) to comply with the repo .editorconfig: replace leading space
indentation in the file (including the import line and the block that defines
iconId, size, iconEl, and the $effect) with tabs, preserving existing code and
line endings (LF); ensure the blocks referencing setIcon, iconEl, iconId, size,
and the $effect remain unchanged except for indentation so the component
behavior is identical after reformatting.

In `@src/gui/MacroGUIs/Components/AIAssistantCommand.svelte`:
- Around line 5-17: Extract the repeated child-prop contract into a shared
interface and use it in AIAssistantCommand.svelte: create a new interface (e.g.,
CommandChildProps) that includes startDrag, dragDisabled, and onDeleteCommand,
then update the component's prop typing to extend or use CommandChildProps with
IAIAssistantCommand (replace the inline type block currently declaring command,
startDrag, dragDisabled, onDeleteCommand in AIAssistantCommand.svelte); ensure
other sibling command-child components are updated to import and use the same
CommandChildProps so the signatures (startDrag, dragDisabled, onDeleteCommand)
remain consistent across all seven components.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 56de64d4-feb2-40d9-8b32-8ffc0f6c8d07

📥 Commits

Reviewing files that changed from the base of the PR and between d2333fe and 3bd4a5a.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (47)
  • eslint.config.mjs
  • package.json
  • src/gui/ChoiceBuilder/FolderList.svelte
  • src/gui/ChoiceBuilder/choiceBuilder.ts
  • src/gui/ChoiceBuilder/folderListProps.svelte.ts
  • src/gui/ChoiceBuilder/templateChoiceBuilder.ts
  • src/gui/GlobalVariables/GlobalVariablesView.svelte
  • src/gui/MacroGUIs/CommandList.conditional.test.ts
  • src/gui/MacroGUIs/CommandList.reorder.test.ts
  • src/gui/MacroGUIs/CommandList.svelte
  • src/gui/MacroGUIs/CommandSequenceEditor.ts
  • src/gui/MacroGUIs/Components/AIAssistantCommand.svelte
  • src/gui/MacroGUIs/Components/ConditionalCommand.svelte
  • src/gui/MacroGUIs/Components/NestedChoiceCommand.svelte
  • src/gui/MacroGUIs/Components/OpenFileCommand.svelte
  • src/gui/MacroGUIs/Components/StandardCommand.svelte
  • src/gui/MacroGUIs/Components/UserScriptCommand.svelte
  • src/gui/MacroGUIs/Components/WaitCommand.svelte
  • src/gui/MacroGUIs/Components/macroCommands.test.ts
  • src/gui/MacroGUIs/commandListProps.svelte.ts
  • src/gui/PackageManager/ExportPackageModal.svelte
  • src/gui/PackageManager/ExportPackageModal.ts
  • src/gui/PackageManager/ImportPackageModal.svelte
  • src/gui/PackageManager/ImportPackageModal.ts
  • src/gui/choiceList/AddChoiceBox.svelte
  • src/gui/choiceList/ChoiceItemRightButtons.svelte
  • src/gui/choiceList/ChoiceList.callbacks.test.ts
  • src/gui/choiceList/ChoiceList.svelte
  • src/gui/choiceList/ChoiceListItem.svelte
  • src/gui/choiceList/ChoiceView.svelte
  • src/gui/choiceList/MultiChoiceListItem.svelte
  • src/gui/choiceList/choiceListActions.ts
  • src/gui/choiceList/persistenceBoundary.svelte.ts
  • src/gui/choiceList/persistenceBoundary.test.ts
  • src/gui/components/ObsidianIcon.svelte
  • src/gui/components/ObsidianIcon.test.ts
  • src/gui/components/svelte5-pipeline.smoke.test.ts
  • src/gui/shared/dndReorder.test.ts
  • src/gui/shared/dndReorder.ts
  • src/gui/svelte/mountComponent.test.ts
  • src/gui/svelte/mountComponent.ts
  • src/quickAddSettingsTab.ts
  • tests/e2e/conditional-branch-persistence.test.ts
  • tests/obsidian-stub.ts
  • tests/vitest-setup.ts
  • tsconfig.json
  • vitest.config.mts

Comment thread src/gui/choiceList/MultiChoiceListItem.svelte
Comment thread src/gui/components/svelte5-pipeline.smoke.test.ts Outdated
chhoumann added 5 commits May 29, 2026 20:52
…unes conversion

Addresses CodeRabbit review on PR #1248: ObsidianIcon now uses $effect (no onMount/
updateIcon), so the smoke test's flow comment (line 40) and the file docstring
(calling ObsidianIcon a 'legacy' component) were stale. Comment-only; tests unchanged.
vitePreprocess config consumed by svelte-check, vite-plugin-svelte (tests) and
eslint-plugin-svelte; silences the 'no Svelte config found' notice. The production
build is unaffected (esbuild-svelte reads its own options from esbuild.config.mjs).
Verified: svelte-check 0/0, build green, full suite 1429, lint green.
…Choice guard

removeChoiceHelper/updateChoiceHelper/filterChoices now narrow via
isMultiChoice(c): c is IMultiChoice instead of (value as any).choices. Multi-node
clones build a typed IMultiChoice local (no cast). Verified: svelte-check 0/0,
choiceList tests pass, build green.
…ence boundary

Adds Plain<T> branded type + snapshot() helper (persist.svelte.ts). The saveChoices
/ saveCommands sinks now accept only Plain<...>, so handing a live $state proxy to
persistence is a svelte-check ERROR rather than a silent data-loss bug (the class
behind the conditional Then/Else regression). Verified the brand bites (raw proxy ->
'Property [PLAIN] is missing') and that all gates stay green: svelte-check 0/0, build,
full suite.
ChoiceListItem + MultiChoiceListItem pass cmp (new Component()) to MarkdownRenderer.render
as the lifecycle owner but never unloaded it, leaking any registered child components
(pre-existing on master; addresses CodeRabbit review on #1248). Added a no-dependency
$effect whose teardown calls cmp.unload() — disposes on destroy only, preserving visible
behavior. Test: cmpLifecycle.test.ts asserts unload() fires on unmount for both rows.
Verified: svelte-check 0/0, build green, full suite.
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
src/gui/choiceList/cmpLifecycle.test.ts (1)

26-39: ⚡ Quick win

Capture the baseline right before unmount() to actually assert teardown.

before is read from a fresh spy (always 0), so the assertion only proves unload was called sometime during render or destroy. Capturing it just before unmount() makes the test specifically verify the destroy-time unload, which is the regression this file guards.

♻️ Move the baseline capture below render
 		const spy = vi.spyOn(Component.prototype, "unload");
-		const before = spy.mock.calls.length;
 		const choice = { id: "a", name: "Alpha", type: "Template", command: false } as unknown as IChoice;

 		const { unmount } = render(ChoiceListItem, {
 			props: { choice, app: new App() as never, roots: [choice], dragDisabled: true, startDrag: noop, actions: actions() },
 		});
+		const before = spy.mock.calls.length;
 		unmount();
 		flushSync();

Same applies to the MultiChoiceListItem test (Lines 41-56).

🤖 Prompt for 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.

In `@src/gui/choiceList/cmpLifecycle.test.ts` around lines 26 - 39, Move the
baseline capture of the spy's call count to just before calling unmount so the
test asserts unload was invoked during teardown: after rendering ChoiceListItem
(render(ChoiceListItem, ...)) but before unmount() read spy.mock.calls.length
into the variable currently named before and then call unmount(), flushSync(),
and assert the call count increased; apply the same change to the
MultiChoiceListItem test — both reference Component.prototype.unload, the spy
variable, the before variable, render(...), and unmount().
🤖 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.

Nitpick comments:
In `@src/gui/choiceList/cmpLifecycle.test.ts`:
- Around line 26-39: Move the baseline capture of the spy's call count to just
before calling unmount so the test asserts unload was invoked during teardown:
after rendering ChoiceListItem (render(ChoiceListItem, ...)) but before
unmount() read spy.mock.calls.length into the variable currently named before
and then call unmount(), flushSync(), and assert the call count increased; apply
the same change to the MultiChoiceListItem test — both reference
Component.prototype.unload, the spy variable, the before variable, render(...),
and unmount().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c3da599e-2ea1-40c7-b1c9-3681beaa05da

📥 Commits

Reviewing files that changed from the base of the PR and between c79fb5b and 8d61ac8.

📒 Files selected for processing (8)
  • src/gui/MacroGUIs/CommandList.svelte
  • src/gui/MacroGUIs/commandListProps.svelte.ts
  • src/gui/choiceList/ChoiceListItem.svelte
  • src/gui/choiceList/ChoiceView.svelte
  • src/gui/choiceList/MultiChoiceListItem.svelte
  • src/gui/choiceList/cmpLifecycle.test.ts
  • src/gui/svelte/persist.svelte.ts
  • svelte.config.mjs
✅ Files skipped from review due to trivial changes (1)
  • svelte.config.mjs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/gui/MacroGUIs/commandListProps.svelte.ts
  • src/gui/choiceList/ChoiceListItem.svelte
  • src/gui/choiceList/MultiChoiceListItem.svelte

…ks.lock)

The ephemeral scheduler lock (sessionId/pid) was accidentally swept into c79fb5b by a
git add -A. Untrack it and gitignore .claude/scheduled_tasks.{lock,json} (machine-local
runtime state). File kept on disk.
@chhoumann chhoumann merged commit 40da1db into master May 29, 2026
9 checks passed
@chhoumann chhoumann deleted the feat/svelte5-rewrite branch May 29, 2026 20:31
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.

Migrate to Svelte 5

1 participant