Skip to content

refactor(web): migrate operator UI to Svelte 5 runes (core, behavior-preserving)#87

Merged
HardcoreMonk merged 13 commits into
mainfrom
feature/svelte5-runes
Jul 19, 2026
Merged

refactor(web): migrate operator UI to Svelte 5 runes (core, behavior-preserving)#87
HardcoreMonk merged 13 commits into
mainfrom
feature/svelte5-runes

Conversation

@HardcoreMonk

@HardcoreMonk HardcoreMonk commented Jul 19, 2026

Copy link
Copy Markdown
Owner

요약

anvil operator Web UI(web/)의 19개 컴포넌트를 Svelte 4 legacy 반응성에서 Svelte 5 core runes로 마이그레이션한다 — export let$props, $:$derived/$effect, 지역 반응 let$state, 부모-바인딩 프롭→$bindable. behavior-preserving. stores·on:·createEventDispatcher·$store 자동구독은 유지(runes 모드에서 동작, 스코프 밖). props/$: 없는 나머지 ~11개는 legacy 유지(혼합 모드 지원).

변경 (SDD 7 태스크)

  • svelte-check 게이트 신설 (2a6e229+4174d9a): web/package.json devDep svelte-check+typescript@^6, web/jsconfig.json, "check": "svelte-check --output human --compiler-warnings non_reactive_update:error"놓친 $state를 failing error로 승격(teeth-verified: 누락 시 exit1). on: deprecation 경고는 예상·비차단.
  • 컴포넌트 마이그레이션 6 태스크 (2fcd108/5440c3f/e588627/fcd556a/2a04fd2): 19개 → $props/$state/$derived/$effect/$bindable. 각 태스크 spec✅/Approved.
  • uidist 재빌드 (1a8a5e2): //go:embed되는 cmd/goose-daemon/uidist/ 재생성(번들 223→217kB, runes 더 경량).
  • TaskPanel Critical 수정 (4184137): 아래 참조.

검증

  • SDD: 태스크별 2단계 리뷰 각 Approved. 최종 whole-branch 리뷰(Opus).
  • 최종 리뷰가 Critical 1건 포착·수정: TaskPanel의 messages = messages // trigger reactivity(Svelte4 반응성 강제 관용구)는 $state에서 strict-=== no-op이고, 스트리밍이 프록시를 우회하는 raw 캡처 ref를 변이 → assistant progress/output/error가 렌더 안 됨. 리뷰어가 컴파일+실행으로 실증(positive control 포함). 6개 태스크 리뷰 모두 놓친(Task3는 "harmless"로 오판) 진짜 behavior regression. 수정: raw a 제거, const aIdx=messages.length-1 후 모든 write를 messages[aIdx].{progress,output,error,done}로 프록시 경유(index). 재검증: 리뷰어가 fixed 패턴 실증 재실행 → progress 라이브 렌더·output 렌더·done flip 확인(pre-fix는 0::false frozen). TaskPanel이 이 패턴의 유일 컴포넌트(grep 확인).
  • 게이트: whole-tree svelte-check 0 errors(114 warn = 전부 on: deprecation, 스코프 유지), npx vite build OK, go build ./... OK(daemon이 새 uidist embed), gofmt -l . empty(CI 게이트 — .go 무변경).
  • DOM-safe(ADR-013): 무관·불변({@html}/innerHTML 없음).
  • 헤드리스 boot-check: login 뷰 렌더, stores/i18n init, 런타임/rune 에러 0(예상 /vms·favicon 404만).

비목표

on:onevent, createEventDispatcher→callback props, stores→$state 모듈, snippet, vitest/컴포넌트 테스트, props/$: 없는 컴포넌트 전환.

참고

  • $bindable은 정적 게이트가 못 잡음(missed $bindable = 런타임 에러) — 2개 프롭(BuiltinPicker.selected/ModelPicker.value)은 정확 명시 + 부모 바인딩 4곳이 $state 로컬로 round-trip함을 리뷰로 검증.
  • env 함정(memory 기록): web/의 node_modules가 스테일(svelte4)일 수 있음 → npm ci 필요; web은 CI 미빌드.
  • 비차단 minor 2: ModelPicker $effect 주석 문구(결론 정확, 이유 부정확), ProfileModal 3 doc-comment 그룹핑(cosmetic).

SDD / lifecycle

spec docs/superpowers/specs/2026-07-19-svelte5-runes-migration-design.md, plan docs/superpowers/plans/2026-07-19-svelte5-runes-migration.md. brainstorming→writing-plans→SDD. lifecycle: Full(대형 리팩터, behavior-preserving).

Summary by CodeRabbit

  • New Features

    • Updated the web interface bundle with management flows for virtual machines, sessions, snapshots, agent groups, settings, monitoring, and system administration.
    • Added built-in English/Korean localization switching with saved preferences.
  • Improvements

    • Migrated core interface components to modern Svelte reactivity while preserving existing behavior.
    • Improved streamed task output updates for smoother progress display.
  • Documentation

    • Added migration guidance and verification steps for the updated interface architecture.

…ctive_update to failing error

Default svelte-check under CLAUDECODE emits machine output and reports
non_reactive_update as a non-failing WARNING (exit 0), so a missed $state would
false-green. --output human + --compiler-warnings non_reactive_update:error makes
it a hard gate: verified legacy=pass(exit0), missed-$state=fail(exit1),
on:click deprecation=warning-only(exit0, kept in scope).
…ctive_update) but NOT $bindable (runtime error; smoke-verified)
…$state array

Final-review Critical: the migrated `messages = messages // trigger reactivity`
self-assign is a strict-=== no-op under $state, and the streaming code mutated a
raw captured ref that bypasses the proxy — so assistant progress/output/error never
rendered (a behavior regression the migration introduced; Svelte 4 let-reassign
forced reactivity, $state does not). Now mutate messages[aIdx].{progress,output,
error,done} by index through the proxy (reviewer's verified positive-control pattern).
svelte-check 0 errors, vite build OK, uidist rebuilt.
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR migrates 19 Svelte components from legacy reactivity to Svelte 5 runes, adds svelte-check configuration and migration documentation, and regenerates the embedded daemon UI bundle with a new hashed asset reference.

Changes

Svelte 5 migration

Layer / File(s) Summary
Migration design and scope
docs/superpowers/plans/..., docs/superpowers/specs/...
Documents the 19-component migration scope, rune conversion rules, invariants, verification steps, and non-goals.
Validation tooling
web/jsconfig.json, web/package.json
Adds Svelte checking configuration, the check script, and svelte-check/TypeScript development dependencies.
Component state and derived values
web/src/components/*.svelte
Replaces legacy props and reactive declarations with $props(), $state(...), and $derived(...) across the target components.
Bindable props and streaming updates
web/src/components/ModelPicker.svelte, web/src/components/TaskPanel.svelte, web/src/components/BuiltinPicker.svelte
Adds bindable props, converts picker synchronization to $effect, and updates streamed task messages through indexed state entries.
Embedded bundle regeneration
cmd/goose-daemon/uidist/...
Adds the rebuilt hashed JavaScript asset and updates index.html to load it.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: steve-seungeui

🚥 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 clearly summarizes the main change: migrating the operator UI to Svelte 5 runes while preserving behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/svelte5-runes

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 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 `@docs/superpowers/plans/2026-07-19-svelte5-runes-migration.md`:
- Line 286: Update the JavaScript code fence in the migration plan to include
the js language identifier, changing the opening fence from an unlabeled fence
to ```js while preserving the example content.
- Around line 1-7: Translate the prose in the Svelte 5 runes migration plan to
Korean, including the title, worker guidance, goal, and architecture
description. Preserve code identifiers, Svelte rune names, paths, commands, file
names, component counts, and other contract-specific values exactly as written.
- Line 206: Fix the Markdown table syntax by escaping every embedded || operator
in docs/superpowers/plans/2026-07-19-svelte5-runes-migration.md at lines 206 and
253-255, and in
docs/superpowers/specs/2026-07-19-svelte5-runes-migration-design.md at lines
56-57; preserve the displayed code while preventing the operators from being
interpreted as table separators.
- Around line 75-85: Update the documented web/package.json check script in Step
3 to pass the warning-promotion option required by the migration gate, ensuring
non_reactive_update warnings are treated as errors while preserving the existing
jsconfig path.

In `@docs/superpowers/specs/2026-07-19-svelte5-runes-migration-design.md`:
- Line 26: Update the component-list code fence in the Svelte 5 runes migration
design document to include a language identifier, using text or another
appropriate identifier, while preserving the fenced content.
- Around line 36-42: Update the bindable-props migration guidance to state that
non-$bindable bindings are detected at runtime through the bind_not_bindable
error, not by svelte-check at compile time. Retain the manual smoke test
requirement and keep the identified BuiltinPicker.selected and ModelPicker.value
cases unchanged.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fc730cad-ec3b-41d3-9b98-fa5f2cfa6100

📥 Commits

Reviewing files that changed from the base of the PR and between 2d39130 and 4184137.

⛔ Files ignored due to path filters (1)
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (26)
  • cmd/goose-daemon/uidist/assets/index-CMV4roMS.js
  • cmd/goose-daemon/uidist/assets/index-CVS1qcEN.js
  • cmd/goose-daemon/uidist/index.html
  • docs/superpowers/plans/2026-07-19-svelte5-runes-migration.md
  • docs/superpowers/specs/2026-07-19-svelte5-runes-migration-design.md
  • web/jsconfig.json
  • web/package.json
  • web/src/components/ActivityFeed.svelte
  • web/src/components/AddAgentModal.svelte
  • web/src/components/BroadcastModal.svelte
  • web/src/components/BuiltinPicker.svelte
  • web/src/components/BuiltinsModal.svelte
  • web/src/components/ChangeRoleModal.svelte
  • web/src/components/CreateFlockModal.svelte
  • web/src/components/FlockDetail.svelte
  • web/src/components/ModelPicker.svelte
  • web/src/components/Monitoring.svelte
  • web/src/components/ProfileModal.svelte
  • web/src/components/RestoreModal.svelte
  • web/src/components/SendTaskModal.svelte
  • web/src/components/Settings.svelte
  • web/src/components/SnapshotModal.svelte
  • web/src/components/SystemPromptModal.svelte
  • web/src/components/TaskPanel.svelte
  • web/src/components/VMDetail.svelte
  • web/src/components/WatchdogPanel.svelte

Comment on lines +1 to +7
# Svelte 5 runes migration (core) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Migrate the 19 operator-UI components under `web/src/components/` that use legacy Svelte 4 reactivity to Svelte 5 runes (`$props`/`$state`/`$derived`/`$effect`/`$bindable`), behavior-preserving, gated by a new `svelte-check` step.

**Architecture:** Hand-migrate component-by-component (grouped into tasks). Each component enters runes mode atomically: `export let`→`$props` (+ `$bindable` for parent-bound props), `$:`→`$derived`/`$effect`, and every reactive local `let`→`$state`. A new `svelte-check` gate (`--compiler-warnings non_reactive_update:error`) mechanically catches a missed `$state` (promoted to a failing error). A missed `$bindable` is a Svelte *runtime* error the static gate does NOT catch — the 2 bindable props are specified exactly and verified by the Task 7 manual smoke. Mixed legacy+runes across components is fully supported, so the app builds and works at every step. Stores, `on:` directives, `createEventDispatcher`, and `$store` auto-subscription are unchanged.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Translate the plan prose to Korean.

As per coding guidelines: **/*.md: 문서는 한국어로 작성하되 코드 식별자, API 경로, 환경 변수, 명령어, 파일명 등 계약을 구성하는 값은 원문을 유지한다. The plan is predominantly English; translate its prose while preserving identifiers, commands, paths, and API names.

🤖 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 `@docs/superpowers/plans/2026-07-19-svelte5-runes-migration.md` around lines 1
- 7, Translate the prose in the Svelte 5 runes migration plan to Korean,
including the title, worker guidance, goal, and architecture description.
Preserve code identifiers, Svelte rune names, paths, commands, file names,
component counts, and other contract-specific values exactly as written.

Source: Coding guidelines

Comment on lines +75 to +85
- [ ] **Step 3: Add the `check` script to `web/package.json`**

In `web/package.json` `"scripts"`, add `check` alongside the existing `dev`/`build`/`preview`:
```json
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./jsconfig.json"
},
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the documented check command enforce the stated gate.

Line 83 omits the warning promotion required by Lines 21 and 143, so non_reactive_update would remain non-fatal.

Proposed fix
-    "check": "svelte-check --tsconfig ./jsconfig.json"
+    "check": "svelte-check --tsconfig ./jsconfig.json --output human --compiler-warnings \"non_reactive_update:error\""
📝 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
- [ ] **Step 3: Add the `check` script to `web/package.json`**
In `web/package.json` `"scripts"`, add `check` alongside the existing `dev`/`build`/`preview`:
```json
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./jsconfig.json"
},
```
- [ ] **Step 3: Add the `check` script to `web/package.json`**
In `web/package.json` `"scripts"`, add `check` alongside the existing `dev`/`build`/`preview`:
🤖 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 `@docs/superpowers/plans/2026-07-19-svelte5-runes-migration.md` around lines 75
- 85, Update the documented web/package.json check script in Step 3 to pass the
warning-promotion option required by the migration gate, ensuring
non_reactive_update warnings are treated as errors while preserving the existing
jsconfig path.

|---|---|---|
| `Settings:25` | `$: availableProviders = providers.filter((p) => p.available)` | `let availableProviders = $derived(providers.filter((p) => p.available))` |
| `CreateFlockModal:28` | `$: defaultProfile = profiles.length ? profiles[0].name : 'default'` | `let defaultProfile = $derived(profiles.length ? profiles[0].name : 'default')` |
| `CreateFlockModal:69` | `$: tokenRows = result ? Object.entries(result.agent_tokens || {}) : []` | `let tokenRows = $derived(result ? Object.entries(result.agent_tokens || {}) : [])` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the Markdown syntax errors in both documents.

Unescaped || operators are being parsed as table separators, causing MD056 column-count errors.

  • docs/superpowers/plans/2026-07-19-svelte5-runes-migration.md#L206-L206: escape the || operator.
  • docs/superpowers/plans/2026-07-19-svelte5-runes-migration.md#L253-L255: escape all embedded || operators.
  • docs/superpowers/specs/2026-07-19-svelte5-runes-migration-design.md#L56-L57: escape the embedded || operators.
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 206-206: Table column count
Expected: 3; Actual: 7; Too many cells, extra data will be missing

(MD056, table-column-count)

📍 Affects 2 files
  • docs/superpowers/plans/2026-07-19-svelte5-runes-migration.md#L206-L206 (this comment)
  • docs/superpowers/plans/2026-07-19-svelte5-runes-migration.md#L253-L255
  • docs/superpowers/specs/2026-07-19-svelte5-runes-migration-design.md#L56-L57
🤖 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 `@docs/superpowers/plans/2026-07-19-svelte5-runes-migration.md` at line 206,
Fix the Markdown table syntax by escaping every embedded || operator in
docs/superpowers/plans/2026-07-19-svelte5-runes-migration.md at lines 206 and
253-255, and in
docs/superpowers/specs/2026-07-19-svelte5-runes-migration-design.md at lines
56-57; preserve the displayed code while preventing the operators from being
interpreted as table separators.

Source: Linters/SAST tools

- Produces: these 2 in runes mode; the two `$bindable` props preserved so parent `bind:` keeps working.

**`BuiltinPicker.svelte`** prop conversion:
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language identifier to the code fence.

Mark this JavaScript example as ```js to satisfy Markdown linting and improve rendering.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 286-286: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@docs/superpowers/plans/2026-07-19-svelte5-runes-migration.md` at line 286,
Update the JavaScript code fence in the migration plan to include the js
language identifier, changing the opening fence from an unlabeled fence to ```js
while preserving the example content.

Source: Linters/SAST tools


## 전환 대상 (19 컴포넌트, union of export let ∪ `$:`)

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language identifier to the component-list code fence.

Use ```text (or another appropriate identifier) to satisfy Markdown linting.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 26-26: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@docs/superpowers/specs/2026-07-19-svelte5-runes-migration-design.md` at line
26, Update the component-list code fence in the Svelte 5 runes migration design
document to include a language identifier, using text or another appropriate
identifier, while preserving the fenced content.

Source: Linters/SAST tools

Comment on lines +36 to +42
1. `export let x` → `let { x } = $props()`; 기본값 있으면 `let { x = d } = $props()`. 한 컴포넌트의 모든 prop을 하나의 `$props()` 구조분해로 모은다.
2. **bindable props**(부모가 `bind:`로 양방향 바인딩): 반드시 `$bindable()`. 확인된 2건:
- `BuiltinPicker.selected` (부모: BuiltinsModal, ProfileModal) → `let { selected = $bindable() } = $props()`.
- `ModelPicker.value` (부모: Settings, ProfileModal) → `let { value = $bindable() } = $props()`.
- (svelte-check가 non-bindable prop 바인딩을 에러로 잡으므로 누락은 게이트에서 검출된다.)
3. `$: x = expr`(derived, 10건) → `let x = $derived(expr)`.
4. `$:` side-effect(1건, `ModelPicker.svelte:19` `$: if (value && models.includes(value)) custom = false`) → `$effect(() => { if (value && models.includes(value)) custom = false })`. **개별 리뷰**: effect는 렌더 후 실행되고 effect 내 state 변경이 재렌더를 유발하므로, 이 케이스가 `$effect`로 정확히 보존되는지 확인하고(필요 시 파생/이벤트 기반으로 재구성) 무한 루프가 없음을 확인한다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For Svelte 5.56.4, does svelte-check detect binding to a prop that is not declared with $bindable(), or is this only reported at runtime?

💡 Result:

In Svelte 5, attempting to use the bind: directive on a component property that is not explicitly declared with the $bindable rune is treated as a runtime error [1][2]. Svelte-check does not currently report this mismatch at compile time [3]. When you attempt to use bind: on a non-bindable property, Svelte issues a bind_not_bindable error, which manifests at runtime [1]. Because the Svelte 5 reactivity system relies on signals rather than static analysis, the compiler does not always have sufficient information to definitively flag these binding attempts during the build process [4]. Consequently, developers typically discover these issues in the browser console during development [2].

Citations:


Align the $bindable check with runtime behavior. svelte-check does not flag bindings to non-$bindable() props at compile time; keep the manual smoke test and describe this as a runtime bind_not_bindable error instead.

🤖 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 `@docs/superpowers/specs/2026-07-19-svelte5-runes-migration-design.md` around
lines 36 - 42, Update the bindable-props migration guidance to state that
non-$bindable bindings are detected at runtime through the bind_not_bindable
error, not by svelte-check at compile time. Retain the manual smoke test
requirement and keep the identified BuiltinPicker.selected and ModelPicker.value
cases unchanged.

@HardcoreMonk
HardcoreMonk merged commit 42d78c4 into main Jul 19, 2026
9 checks passed
@HardcoreMonk
HardcoreMonk deleted the feature/svelte5-runes branch July 19, 2026 05:18
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.

1 participant