Conversation
仕様確定版の「規約適合 = slugify の不動点」を実装する純関数群。 小文字 [a-z0-9-]・48 文字上限・Windows 予約デバイス名回避・ 連番 suffix (-2 昇順、truncate 後付与) を単一モジュールに集約し、 生成物が常に適合判定を満たすことをテストで保証する。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JSON5 メタ / skill frontmatter への凍結 ID 注入。パースした生内容 への追記のみ (決定的・コメント整形保持・後勝ちで不正値上書き) で、 達成済み判定の内容一致が揮発デフォルトで偽陰性にならないことを 保証する。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
widgets/plugins/columnQueries 共通の sidecarFileCollection を #913 仕様へ 書き換える: - loadAll はファイル名の辞書順 (UTF-8 バイト順) で決定的に処理し、各 アイテムに runtime-only の実ファイル基底名 fileBase を保持する - ID 凍結を常設規則化: メタの ID 欠損 (キー不在・空・非文字列・256 文字超。 制御文字は含めない) はメタファイル完全名を injectJson5Id の最小変換で 書き戻す (冪等・コメント保持) - 同一 ID の 2 件目以降は警告 + 読込スキップ (ファイルは削除しない) - 空ソースフォールバック廃止: 「メタあり・ソースなし」はミラー本文からの 再作成、無ければ readOnly (persist 抑止) で可視化 - persist/delete/rename は fileBase 参照 (name からの再計算を全廃)。 新規割当は slugifyName + resolveAvailable。占有判定は 対応表 ∪ 実列挙 (.is/.meta.json5/.history.json5) ∪ ID 集合を casefold 照合 - 書込順 src → meta / 削除順 meta → src (+ 履歴サイドカーも削除)。 rename は src → meta → history、casefold 一致の自己リネームは中間名 2 段 - migrateItems: 規約外名の copy-adopt (生内容 + 凍結 ID・読み戻し検証・ 達成済み判定・suffix 退避)。sweepHistory: 実列挙の主ファイル basename と 対応の取れない .history.json5 を削除 settingsFs には renameQueryFile と isMainDeckWindow (移行の実行主体判定) を 追加し、不要になった per-kind ファイル名ヘルパーを削除。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 各 store に「初回読込 (対応表確定) + 初回移行」を待つ ready ゲートを 設け、変更系操作 (新規作成・リネーム・保存・削除) のファイル反映を 移行完了後へ直列化する - initFileStorage で (a) 規約外名の copy-adopt 正規化 → (b) ミラー在・ ファイル不在の新 slug 再作成 (空ソースは書かない) → 履歴 sweep を メインウィンドウのみで実行 (isMainDeckWindow・冪等) - リネームを「旧 baseName 削除 + 新名 persist」から rename コマンド追随 (src → meta → history、ID 不変) に置換し、完了を await してから保存する - 保存・削除の直前に localStorage ミラーの対応表を読み直す (別ウィンドウのリネーム後の削除が stale 名で空振りしないように) - 履歴 push/list/revert のキーを fileBase に変更、readOnly 個体の src 編集を抑止、外部エディタ起動 (PluginsContent) を fileBase 参照に変更 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
テーマ (.ndtheme.json5) / スキル (.md) 共通の「単一ファイルで 1 アイテム」 永続化サービス。sidecarFileCollection と同じ #913 不変条件を実装する: - loadAll はファイル名の辞書順 (UTF-8 バイト順) で決定的に処理し、各 アイテムへ runtime-only の fileBase (ID → 実ファイル名の対応表) を付与 - ID 凍結 (常設規則): ID 欠損 (キー不在・空・非文字列・256 文字超) なら 種別の実効値を idFreeze の最小変換で書き戻す (冪等・コメント保持) - 重複 ID は 2 件目以降を警告 + skip (ファイルは削除しない) - 移行 (a) は copy-adopt (読む → 新 slug 名へ生内容を書込 → 検証 → 旧削除)。 達成済み判定 (ID 一致 + 内容一致 → 旧削除のみ / ID 一致・内容不一致 → 削除せず suffix + 警告) と case-only 中間名 2 段を含む - 占有判定は casefold + 実列挙 ∪ 対応表 ∪ (ID を決める操作では) ID 集合 - 履歴 sweep は実列挙の主ファイル basename 集合と casefold 照合 パース形式 (JSON5 / frontmatter) と凍結実効値は config フックで注入し、 テーマとスキルで共用する。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- themeFileSync を singleFileCollection ベースに書き換え、theme オブジェクトの runtime-only な fileBase を対応表とする (ファイルへは projection で書かず、localStorage ミラーには同乗) - ID 凍結 (実効値 custom-<完全ファイル名>)・重複 ID skip・辞書順読込 - initFileStorage で (a) 規約外名の copy-adopt 正規化 → (b) ミラー在・ ファイル不在の新 slug 再作成 (旧 localStorage 片方向移行を統合) → 履歴 sweep をメインウィンドウのみで実行 - 変更系操作は ready ゲート (初回読込 + 移行完了) 待ちに直列化し、 保存・削除の直前にミラーの対応表を読み直す - renameTheme を「旧削除 + 新書込の並行発火」から rename コマンド追随 (主ファイル + 履歴、ID 不変・完了 await 後に persist) へ置換 - 手動テーマ貼り付けの同一 ID 更新で、貼り付けコードに $notedeck が 無ければ既存の $notedeck (storeId 等) を保持する - 履歴キー (pushSnapshot / theme.history / theme.revert) を theme.id から fileBase 参照に変更 - 外部エディタ起動 (ThemeEditorContent) を対応表参照に変更、 themeFilename / listThemes を廃止 (β 方針: 後方互換は残さない) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- skills store を singleFileCollection ベースに書き換え、SkillMeta の runtime-only な fileBase を対応表とする (frontmatter には書かない) - ID 凍結の実効値 = 拡張子を除いた basename (現行 fallbackId と同値)、 重複 ID は 2 件目以降を skip (ファイルは削除しない) - initFileStorage で (a) 規約外名の copy-adopt 正規化 → 履歴 sweep を メインウィンドウのみで実行。スキルは本文ミラーが無いため (b) 再作成は 非適用 (外部削除 = 削除確定)。builtin seed / storeMoved migrate は 既存挙動を維持 - 変更系操作 (add / update / remove) を ready ゲート (初回読込 + 移行 完了) 待ちに直列化。新規作成の fileBase は表示名 slug から割当 (ファイル名は ID からでなく対応表から) - 表示名変更はファイル rename で追随 (ID 不変・主ファイル + 履歴、 完了 await 後に persist)。削除は主ファイル + 履歴を消す - 履歴キー (pushSnapshot / skills.history / skills.revert) を name || id から fileBase 参照に変更 - 外部エディタ起動 (SkillEditContent) を対応表参照に変更、 skillFilename / listSkillFiles を廃止 (β 方針: 後方互換は残さない) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
builtin seed のスキルはテンプレ id (slug 適合) をそのままファイル名に する。表示名 slug だとフレッシュインストールで notedeck.md / notedeck-2.md / skill.md に化けて意味が消えるため。占有時は連番、 規約不適合なら表示名 slug に落ちる。sidecar 側にも同じフックを足し、 PR-3 のストアインストール (ファイル名 = storeId) が同じ経路に乗れる ようにする。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
唯一「ID = ファイル名」で結合していたプロファイルを singleFileCollection の対応表 (fileBase) 方式へ移す。 - codec: id をファイルに書き、parse 済み raw を土台に既知フィールドを 上書きして未知フィールドを保持する (ダウングレード往復の剥がれ防止)。 fileBase は runtime-only でファイルへ書かない - ID 凍結: id 欠損は拡張子込みの旧完全ファイル名を実効値として注入。 既存の nd-deck-active-profile / ?profile= / windowProfileId は無追随で 生き続ける - リネーム = 表示名変更のみ (ID 不変)。ファイルは rename 追随し、完了を await してから保存する (旧 rename/persist 並行発火の順序バグ根絶) - 移行 (a) 規約外名の copy-adopt / (b) ミラー在・ファイル不在の再作成を メインウィンドウのみで実行。memOnly マージ同定を「ID 一致 or 名前+作成日時一致」に拡大 - 新規作成は slug 形式の ID + slug ファイル名。変更系操作は ready ゲート (初回読込 + 移行完了) を待つ。保存・削除の直前にミラーの対応表を読み直す - 外部エディタ起動は対応表のファイル名で開く Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
skillsFileSync.test の先例に倣い、settingsFs をインメモリ疑似 FS で モックして store 経由の不変条件を保証する: - ファイル内 id の採用と fileBase 対応表の保持 - id 欠損の凍結 (= 旧完全ファイル名) と規約外名の copy-adopt 正規化 - 凍結による activeProfileId / windowProfileId / デッキ内容の無追随整合 - リネームの ID 不変 + ファイル rename 追随 - 新規作成・デフォルトプロファイルの slug ファイル名/ID - 移行 (b) ミラー再作成と memOnly マージ同定の拡大 - 保存・削除直前のミラー対応表読み直し (別ウィンドウのリネーム追随) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
custom.css.history.json5 が root allowlist から漏れており、フロントの settingsFs 履歴系がこの名前で read/write を試みて常に reject されていた。 回帰テストは次コミットの import_bundle 強化テスト群と同居。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
import_bundle を強化する: - キー構造検証: 「許可サブディレクトリ + ファイル名」の 2 要素、または 許可ルートファイルの 1 要素のみ。3 要素以上のネストは拒否。サブディレクトリ 側のファイル名には強化検証 (制御文字・Windows 予約デバイス名 (stem 判定)・ 先頭ドット・末尾ドット/空白) を適用。文字種 (日本語等) は寛容のまま — 旧バックアップの復元を拒否しない。validate_filename 本体は強化しない (規約外名の既存ファイルの読取・移行が成立しなくなるため) - casefold 衝突検査 + 排他書込: 書込先ディレクトリの実列挙 + ASCII casefold の事前照合の上で、create_new 予約 → atomic_write (tmp + rename)。FS 自身の 同名解決 (非 ASCII casefold・NFC/NFD) を衝突検出の正とし、case-insensitive FS で旧バックアップが正規化済みファイルを truncate 置換するのを防ぐ - 衝突判定・skip/suffix は basename グループ (ソース + メタ + 履歴) 単位。 全構成が内容バイト一致ならグループ skip、不一致なら全構成を同一の -2 昇順 suffix (複合拡張子の前に挿入) へ退避し、ファイル内 ID は変えない - sessions/ 配下は 0o600 を維持。素の fs::write を全廃 - 許可ルートファイルは固定名の単一ファイルなので復元 = atomic 置換 (suffix 退避先は allowlist 外で二度と読まれないため衝突分岐は不適用) - import_settings_json がスキップ / 別名退避の警告リストを返し、 BackupContent.vue が再起動前に件数 + 内容を開示する Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ストアインストール経路の同一性統一 (misstore 側は次コミット) の store 基盤: - widget/plugin/query のメタと skill frontmatter、テーマ $notedeck に storeSha512 / storeVersion を追加 (更新検知 #1040 の baseline。 既存インストール品の未記録はそのまま — ここでは記録し始めるだけ) - 各コレクション config に preferredBase: storeId を配線し、 新規ストアインストールのファイル名 = storeId にする (規約不適合は 自動で表示名 slug に落ち、占有時は連番 suffix) - applyStoreUpdate を widgets/plugins/columnQueries に追加: 本体 (src) と ストア由来メタを上書きし、ローカル値 (改名 name・active・スコープ・ configData・autoRun) は維持。ソース欠損の readOnly 個体は検証済み 配布ソースで復旧する - persist 経路で live 要素 (reactive proxy) を渡すよう修正: raw 参照だと 占有判定の「自分自身は占有とみなさない」が崩れ、preferredBase = id/storeId のファイル名に無意味な -2 suffix が付く (builtin seed にも 効く潜在バグ) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
既存判定・更新判定の照合キーを全種別で storeId のみに統一し、 「storeId 一致の既存あり → 上書き更新 / なし → 新規作成」に揃える: - installSkill: frontmatter の id 宣言優先を廃止。新規のローカル ID = storeId (レジストリのディレクトリ名が正)、既存への更新はローカル ID 維持。storeId 不一致の既存 ID に占有されていたら上書きせず連番 suffix - installTheme: t.id === entry.id フォールバックを廃止。更新は配布内 UUID が変わってもローカル ID・ローカル改名を維持して既存を置換 (既存 fileBase のファイルへ書く) - installPlugin: isDuplicate(name) の名前重複拒否を廃止 (同名自作が あってもインストール可能 — ファイル名は suffix が捌く)。linkScope-only も廃止し、本体とストア由来メタを上書き + scope 追加 - installWidget: 既存ありの early return を廃止し上書き更新に - installQuery: 更新で name を渡さない (ローカル改名維持) - isInstalled / isThemeInstalled / isSkillInstalled: name・内部 ID 照合の残りを全廃 (deep link の同定もこの経路で storeId に統一) - 全種別でインストール/更新時に storeSha512 (computeSha512 で照合済みの 値) と storeVersion を記録 更新で維持されるローカル値: 有効/無効・実行モード・スコープ・設定値・ autoRun・ローカル改名 (name は新規時のみ entry 値を採用) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
widget/plugin/query も新規インストール時のローカル ID = storeId とする (仕様確定版の「スキル含む全種別」)。アンインストール → 再インストール でカラム参照 (widgetIds / noteQueryRefs) が生き残る。既存 ID との衝突 時のみ連番 suffix (skills と同じガード)。既存インストール品の ID は 凍結どおり不変。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
仕様の「警告 + UI 通知」のうち UI 通知が console.warn 止まりだった 残件。コレクションに notify フックを足し、全 6 種別の設定を 警告トーストへ配線する (ユーザー操作なしには解消しない状態のため)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codemirror/merge の unified ビューで oldText/newText の全文 diff を 読取専用表示する。言語モードは既存基盤 (AiScript 自前モード / lang-json / lang-markdown / lang-css) から解決し、挿入/削除色は グローバル CSS 変数で開放する。未変更領域は既定で折りたたむ。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
diff 指定時は code ブロックの代わりに CodeDiffView を描画する (併用時は diff 優先)。attribution・remember 等の既存挙動は不変。 ダイアログ内は最大高さを制限して diff 側にスクロールを持たせる。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
インストール時に記録した storeSha512 と registry 現行 sha512 の比較で 更新を検知する (version 文字列は bump が機械強制されていないため判定に 使わない)。storeSha512 未記録の既存インストール品は、レジストリ照会時に 現行値を無通知で基準記録し、次の変更から検知を始める (誤って「全件 更新あり」にしない)。各 store には履歴 push を伴わない recordStoreBaseline を追加した。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
配布アイテム 5 種の更新適用 (updatePlugin/Theme/Widget/Skill/Query) を追加。 ソース fetch + sha 照合の後、ConfirmOptions.diff で適用前の全文 diff を確認し (old = ローカルの現在の本体、new = 適用後の本体)、承認されたら確認に使った 内容をそのまま適用する — 承認後の再 fetch・再計算はしない (#981 不変条件)。 表示の主はレジストリ updatedAt、version は補助。 再同意: プラグインの permissions が拡大する更新は「新しい権限: xxx」を warning で明示する。既存記録が無いなど判定不能な変化は拡大側に倒す。 リトライ: インストール/更新でソース sha が entry.sha512 と不一致のとき、 改ざん警告の前にレジストリ index をキャッシュ無効化して再取得し 1 回だけ リトライする (ストア更新直後の TTL 内は正常な更新が偽警告になるため)。 install 系 5 経路も同じ検証付き取得 (fetchVerifiedSource) に統一した。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
インストール済み + ストア側更新ありのカードで「インストール済み」表示を 「更新あり」バッジ + 「更新」ボタンに置き換える。5 種 (ウィジェット / プラグイン / テーマ / クエリ / スキル) で同じ文言・同型の accent チップ。 - 更新ボタンは store の updateXxx を呼ぶだけ (diff 付き確認・権限拡大の 明示・sha 照合は store 側)。実行中は installing 系 ref で無効化 - バッジ/ボタンの tooltip は主 = レジストリ updatedAt、補助 = version - テーマは grid タイルの流儀に合わせ、オーバーレイチップ + 丸ボタン。 カードクリックも更新に接続 (インストールと同じ入口)。更新は installedFor に触れず既存の適用範囲を維持 - WidgetCard の dom テストを追加 (更新ボタン表示 / update emit / 無効化) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
API surface diff外部アプリ向け API 面 ( Full diffdiff --git a/src-tauri/openapi.json b/src-tauri/openapi.json
index 040f21b..79287a7 100644
--- a/src-tauri/openapi.json
+++ b/src-tauri/openapi.json
@@ -6,7 +6,7 @@
"license": {
"name": "MIT"
},
- "version": "1.46.2"
+ "version": "1.47.0"
},
"paths": {
"/api": {
diff --git a/src/bindings.ts b/src/bindings.ts
index d697552..fe068e4 100644
--- a/src/bindings.ts
+++ b/src/bindings.ts
@@ -2329,7 +2329,7 @@ async exportSettingsJson() : Promise<Result<boolean, { code: string; message: st
*
* @see src-tauri/src/commands/settings.rs
*/
-async importSettingsJson() : Promise<Result<boolean, { code: string; message: string; apiCode: string | null }>> {
+async importSettingsJson() : Promise<Result<ImportSettingsResult, { code: string; message: string; apiCode: string | null }>> {
try {
return { status: "ok", data: await TAURI_INVOKE("import_settings_json") };
} catch (e) {
@@ -3223,6 +3223,12 @@ export type HttpFetchResponse = { status: number; headers: Partial<{ [key in str
* 画像ディスクキャッシュの使用量 (#815)。設定のキャッシュ画面で表示する
*/
export type ImageCacheStats = { bytes: number; files: number }
+/**
+ * import_settings_json の結果。`imported: false` はダイアログのキャンセル。
+ * `warnings` はスキップ / 別名退避したエントリの説明 (#913 付随修正 — フロントは
+ * 復元完了メッセージに件数 + 内容を表示する)。
+ */
+export type ImportSettingsResult = { imported: boolean; warnings: string[] }
export type JsonValue = null | boolean | number | string | JsonValue[] | Partial<{ [key in string]: JsonValue }>
/**
* Misskey の `mutedWords` / `hardMutedWords` の 1 要素。
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
📝 WalkthroughWalkthroughThe release adds stable slug-based settings persistence, validated settings import warnings, registry metadata and update flows for distributed items, read-only diff confirmation, and file-base-aware history and external-editor handling. ChangesSettings persistence and migration
Store updates and confirmation UI
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
Deploying notedeck with
|
| Latest commit: |
bdacec5
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://d735d079.notedeck-d3a.pages.dev |
| Branch Preview URL: | https://develop.notedeck-d3a.pages.dev |
lru 0.16 の LruCache::pop() が panic 安全でなく、キーの Drop が panic すると解放済みノードがリンクリストに残り use-after-free に なりうる (0.18.2 で修正)。API 変更はなく、既存の image_cache / ogp の利用箇所はそのまま動く。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (10)
src/services/singleFileCollection.test.ts (1)
444-549: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win移行テストに履歴サイドカーのケースがありません。
renameItemFilesのテストは履歴サイドカーの追随を検証しますが (Line 378-392)、migrateItemsのテストは主ファイルだけを置いています。このためsrc/services/singleFileCollection.tsのcopyAdoptOneが履歴を移送しない欠落を検出できません。Bad Name.history.json5を含むケースを追加してください。💚 追加テスト案
+ it('copy-adopt は履歴サイドカーも新 basename へ追随させる', async () => { + const fs = makeFakeFs({ + [`Bad Name${EXT}`]: file('i1', 'Bad Name'), + 'Bad Name.history.json5': '{ entries: [1] }', + }) + const col = makeCollection(fs) + const { items } = await col.loadAll() + await col.migrateItems(items) + expect(items[0]?.fileBase).toBe('bad-name') + expect(fs.files.has('bad-name.history.json5')).toBe(true) + expect(fs.files.has('Bad Name.history.json5')).toBe(false) + })🤖 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/services/singleFileCollection.test.ts` around lines 444 - 549, migrateItems の移行テストに履歴サイドカー追随のケースを追加してください。特に copy-adopt 経路を検証するため、`Bad Name` の主ファイルと `Bad Name.history.json5` を用意して `migrateItems` を実行し、新しい slug 側へ履歴内容が移送され、旧名側の主ファイルと履歴サイドカーが削除されることを確認してください。src/stores/deckProfileFileSync.test.ts (1)
229-245: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a test for the delete-then-undo file outcome.
The delete test asserts only that the file disappears.
deleteProfilereturns an undo callback that callssaveProfiles, which re-writes files through a separatereadycontinuation. That interleaving is the risk described insrc/stores/deckProfile.ts.Add a case that calls the returned undo callback immediately after
deleteProfileand asserts that the file exists afterwards.🤖 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/stores/deckProfileFileSync.test.ts` around lines 229 - 245, The delete test in deckProfileFileSync.test.ts only verifies file removal; extend it to capture the undo callback returned by deleteProfile, invoke it immediately, and then wait for and assert that the profile file exists again. Keep the existing setup and deletion assertion as appropriate, and cover the saveProfiles/ready continuation outcome.src/stores/deckProfile.ts (1)
591-601: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove the merge predicate into
src/services/.Lines 595-601 implement a merge rule: a mirror profile is memory-only when no file profile matches by
idor byname+createdAt. This is pure logic. It currently has no direct unit test and is reachable only through the store.Extract it as a named function in
src/services/(for example next todeckProfileFiles) and unit test it directly. Keep the store limited to subscription, cache, and UI state.Based on learnings: 「正規化、マイグレーション、マージ規則、ファイル codec などの純ロジックは store に置かず
src/services/に実装し、直接ユニットテストする。store は購読、キャッシュ、UI 状態に限定する」.🤖 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/stores/deckProfile.ts` around lines 591 - 601, The merge predicate currently embedded in the store should be extracted into a named pure function under src/services/, near the existing deck profile file logic such as deckProfileFiles. Move the “matches by id or by name and createdAt” filtering rule into that function, update the store to call it, and add direct unit tests covering the matching and memory-only cases.Source: Learnings
src/components/window/PluginsContent.vue (1)
96-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe four external-file call sites handle a missing
fileBasedifferently. Three windows return a descriptor withdisabled: true, which keeps a visible but inactive affordance. One window returnsnull, which removes the affordance. The user sees two different behaviours for the same "file not yet created" state. Pick one convention and apply it everywhere.
src/components/window/PluginsContent.vue#L96-L101: return{ name: '', subdir: 'plugins', disabled: true }whenp.fileBaseis unset, instead ofnull.src/components/window/SkillEditContent.vue#L34-L43: keep thedisabledform; no change needed if thedisabledconvention is chosen.src/components/window/ProfileEditorContent.vue#L112-L128: keep thedisabledform; no change needed if thedisabledconvention is chosen.src/components/window/ThemeEditorContent.vue#L73-L83: keep thedisabledform; no change needed if thedisabledconvention is chosen.🤖 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/components/window/PluginsContent.vue` around lines 96 - 101, The external-file call sites use inconsistent missing-file behavior. In src/components/window/PluginsContent.vue lines 96-101, update the file descriptor logic to return the disabled descriptor with an empty name when p.fileBase is unset instead of null. Keep the existing disabled forms unchanged in src/components/window/SkillEditContent.vue lines 34-43, src/components/window/ProfileEditorContent.vue lines 112-128, and src/components/window/ThemeEditorContent.vue lines 73-83.src/stores/columnQueries.ts (1)
272-294: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value
applyStoreUpdateoverwritesdescriptionandiconUrlwithundefinedwhen the registry omits them.
...patchincludes the optional keys only when the caller passes them. The caller insrc/stores/misstore.ts(lines 1091-1096) always passesdescription: e.descriptionandiconUrl: e.iconUrl, so a registry entry without these fields clears the stored values. This is consistent with "store-owned fields follow the registry", but it also erases values that an older registry version supplied. Confirm this is intended.Consider dropping undefined keys if the intent is to keep the previous values:
♻️ Optional: keep previous values for omitted fields
const next: NamedQueryMeta = { ...prev, - ...patch, + ...Object.fromEntries( + Object.entries(patch).filter(([, v]) => v !== undefined), + ), readOnly: undefined, updatedAt: Date.now(), }🤖 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/stores/columnQueries.ts` around lines 272 - 294, Update applyStoreUpdate so undefined optional patch fields do not overwrite existing NamedQueryMeta values when registry entries omit them. Preserve prev.description and prev.iconUrl unless the patch provides defined replacements, while continuing to apply other patch fields and persist the resulting next value.src/stores/plugins.ts (1)
611-654: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
applyStoreUpdatekeepsconfigDataentries for configuration keys that the update removed.The loop adds defaults for new keys. It does not delete values for keys that no longer exist in
patch.config. The stale values stay in memory and are written to the.meta.json5file. This is harmless for behavior, but it grows the persisted metadata over successive updates.♻️ Optional: drop values for removed configuration keys
if (patch.config) { + for (const key of Object.keys(plugin.configData)) { + if (!(key in patch.config)) delete plugin.configData[key] + } for (const [key, def] of Object.entries(patch.config)) { if (!(key in plugin.configData)) plugin.configData[key] = def.default } }🤖 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/stores/plugins.ts` around lines 611 - 654, Update applyStoreUpdate so plugin.configData is synchronized with patch.config: retain values for keys still defined, add defaults for new keys, and remove entries whose configuration keys were removed. Preserve existing behavior when patch.config is absent.src/stores/columnQueries.test.ts (1)
54-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for
recordStoreBaseline.The new suite covers
applyStoreUpdate. The store also exposesrecordStoreBaseline, which must recordstoreSha512andstoreVersionwithout changingsrcorupdatedAt. That silent-baseline contract is the basis for update detection insrc/stores/misstore.ts(recordQueryBaselines). A regression there would silently disable update detection.I can generate the test if you want.
🤖 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/stores/columnQueries.test.ts` around lines 54 - 58, Add a test suite for the store’s recordStoreBaseline method alongside the applyStoreUpdate tests, verifying it records storeSha512 and storeVersion while leaving src and updatedAt unchanged. Ensure the test covers the baseline behavior used by recordQueryBaselines in misstore.ts.src/capabilities/builtins/plugins.ts (1)
467-467: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExtract the history basename derivation into one helper.
The expression
fileBase ?? (name || installId)is now repeated at Line 467, Line 486, and Line 536. The three sites must stay identical, otherwiseplugins.historyandplugins.revertread different history files. A single helper removes that drift risk.♻️ Optional: single derivation point
+function historyBasename(plugin: PluginMeta): string { + return plugin.fileBase ?? (plugin.name || plugin.installId) +}Then use
historyBasename(plugin)at all three sites.Also applies to: 486-486, 536-536
🤖 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/capabilities/builtins/plugins.ts` at line 467, Extract the repeated plugin history basename expression into a shared historyBasename(plugin) helper, preserving the existing fileBase ?? (name || installId) precedence. Replace the derivation at all three sites in the relevant plugin history/revert flows with calls to this helper so every operation uses the same basename logic.src/components/deck/PluginCard.vue (1)
69-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe
updateTitlecomputed is copied verbatim into two store cards. Both cards derive the same tooltip fromupdatedAtandversionwith identical string templates. One shared composable removes the drift risk and gives a single place to verify theformatDateinput type.
src/components/deck/PluginCard.vue#L69-L76: replace the localupdateTitlecomputed with a shared composable, for exampleuseStoreUpdateTitle(() => props.updatedAt, () => props.version).src/components/deck/QueryCard.vue#L65-L72: remove the duplicate computed and call the same composable.Confirm that
formatDateaccepts the ISO string that the registry supplies inupdatedAt. If it expects aDateor an epoch number, both tooltips renderInvalid Date.🤖 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/components/deck/PluginCard.vue` around lines 69 - 76, Extract the duplicated updateTitle logic into a shared useStoreUpdateTitle composable, then replace the local computed in src/components/deck/PluginCard.vue lines 69-76 and src/components/deck/QueryCard.vue lines 65-72 with calls passing updatedAt and version accessors. Ensure the composable verifies or converts the registry-provided ISO updatedAt value to the input type expected by formatDate, while preserving the existing tooltip text and empty-title behavior.src/stores/widgets.ts (1)
400-428: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
applyStoreUpdateclears a localiconUrlwhen the registry entry omits it.Line 421 assigns
patch.iconUrlunconditionally. If the registry entry has noiconUrl, the stored value becomesundefinedand the widget loses its icon after an update.installWidgetinsrc/stores/misstore.tsuses...(e.iconUrl ? { iconUrl: e.iconUrl } : {})for new installs, so the two paths differ. Confirm that clearing is intended; otherwise assign only whenpatch.iconUrlis defined.🛠️ Proposed fix
- widget.iconUrl = patch.iconUrl + if (patch.iconUrl !== undefined) widget.iconUrl = patch.iconUrl🤖 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/stores/widgets.ts` around lines 400 - 428, Update applyStoreUpdate so widget.iconUrl is changed only when patch.iconUrl is defined, preserving the existing stored icon when the registry omits it. Keep the remaining metadata assignments and persistence behavior unchanged, matching installWidget’s conditional icon handling.
🤖 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-tauri/src/settings_store.rs`:
- Around line 431-441: Update import_group’s reservation-backed write paths
around reserve_all and atomic_write so any member write failure releases all
group reservations before propagating the error. Apply this to both write loops,
including the later fallback path, while preserving the existing successful
cleanup and return behavior described by import_bundle.
In `@src/components/common/CodeDiffView.vue`:
- Around line 97-134: Replace the hardcoded `#1e1e1e` and `#9d9d9d` values in
.diffView and :global(.cm-collapsedLines) with global CSS variables, define
those variables in src/styles/global.css with appropriate light/dark theme
values, and preserve the existing color-mix behavior for the collapsed-line
background while using the new surface variable.
In `@src/components/deck/DeckSkillColumn.vue`:
- Around line 218-221: Validate or normalize each entry returned by
fetchSkills() before assigning it to StoreSkillEntry[]. Ensure updatedAt is
present and represents a valid date, rejecting or safely normalizing invalid
registry entries so storeUpdateTitle() never receives missing or invalid date
data.
In `@src/components/deck/PluginCard.vue`:
- Around line 183-194: Update the update button in PluginCard’s
alreadyInstalled/hasUpdate branch to disable when either installing is true or
capabilityOk is false, matching the install button’s guard and preventing
updates to incompatible versions.
In `@src/components/deck/ThemeCard.vue`:
- Around line 121-129: Update the update button in ThemeCard.vue, identified by
the v-if="alreadyInstalled && hasUpdate" condition, to bind its disabled state
to installing so repeated clicks cannot emit concurrent update events.
In `@src/services/idFreeze.ts`:
- Around line 104-113: Update injectFrontmatterId to derive the opening
frontmatter delimiter length from the matched text, rather than assuming four
characters, so innerEnd remains correct for both LF and CRLF input. Add a CRLF
coverage case in the idFreeze tests verifying the injected ID preserves the
final frontmatter line and document body.
In `@src/services/singleFileCollection.ts`:
- Around line 239-253: Update renameFileSet and its callers so item.fileBase is
changed immediately after the primary file rename succeeds, including each
intermediate case-only rename. Handle the HISTORY_SUFFIX rename independently:
catch failures, emit a warning, and do not roll back or throw after the primary
rename has completed. Preserve the existing target-exists skip behavior.
- Around line 301-392: Update copyAdoptOne in
src/services/singleFileCollection.ts (lines 301-392) to move the old basename’s
.history.json5 sidecar to the finalized basename after main-file migration; skip
when the old sidecar is absent or the destination already exists, including the
case-only two-step and already-adopted branches. Add a migrateItems test in
src/services/singleFileCollection.test.ts (lines 444-549) that creates a
noncanonical main file with its history sidecar and verifies the history exists
under the new basename.
In `@src/stores/deckProfile.ts`:
- Around line 571-583: Update the Tauri initialization flow in ensureDefaults
and initFileStorage so initialized.value is set to true even when
initFileStorage rejects, before resolving readiness in the catch/finally path.
Preserve the existing success behavior and browser branch.
- Around line 165-195: Serialize all profile file operations through one shared
queue: update persistProfileToFile and persistAllProfilesToFiles to enqueue work
after ready rather than attaching independent continuations. In
src/stores/deckProfile.ts lines 437-449, chain the undo saveProfiles operation
after deleteItemFiles completes. In lines 470-482, enqueue renameItemFiles and
persistItem together so concurrent persists cannot use the old basename.
In `@src/stores/misstore.ts`:
- Around line 740-754: Update the existing-plugin branch in installPlugin to
reuse updatePlugin’s confirmation and permission-expansion checks before
applying meta.permissions; if the source is unchanged, limit the operation to
linking the scope without silently updating permissions.
In `@src/stores/skills.ts`:
- Around line 385-398: Update initFileStorage so it captures memoryOnly skills
before the fileSkills.length === 0 early return, then appends those skills to
the seeded built-ins after seedBuiltIns completes. Preserve the existing
non-empty merge behavior and ensure skills added during initialization remain in
skills.value.
In `@src/stores/theme.ts`:
- Around line 467-488: Update renameTheme so it replaces the renamed theme
within installedThemes.value rather than mutating theme.name in place, ensuring
shallowRef consumers observe the change before setStorageJson and file
persistence. Preserve the existing theme lookup, rename synchronization, and
persistence flow.
In `@src/stores/themeFileSync.ts`:
- Around line 2-72: Move the theme codec and migration-related pure logic out of
the store layer into service modules: in src/stores/themeFileSync.ts lines 2-72,
relocate parsing, validation, ID injection, serialization, and the
createSingleFileCollection configuration to a service while preserving its
behavior; in src/stores/theme.ts lines 686-725, relocate memory/file
reconciliation and migration planning to a service. Keep both store locations
limited to applying service results alongside subscription, cache, UI state, and
file operations, and expose the extracted pure logic for direct unit testing.
---
Nitpick comments:
In `@src/capabilities/builtins/plugins.ts`:
- Line 467: Extract the repeated plugin history basename expression into a
shared historyBasename(plugin) helper, preserving the existing fileBase ?? (name
|| installId) precedence. Replace the derivation at all three sites in the
relevant plugin history/revert flows with calls to this helper so every
operation uses the same basename logic.
In `@src/components/deck/PluginCard.vue`:
- Around line 69-76: Extract the duplicated updateTitle logic into a shared
useStoreUpdateTitle composable, then replace the local computed in
src/components/deck/PluginCard.vue lines 69-76 and
src/components/deck/QueryCard.vue lines 65-72 with calls passing updatedAt and
version accessors. Ensure the composable verifies or converts the
registry-provided ISO updatedAt value to the input type expected by formatDate,
while preserving the existing tooltip text and empty-title behavior.
In `@src/components/window/PluginsContent.vue`:
- Around line 96-101: The external-file call sites use inconsistent missing-file
behavior. In src/components/window/PluginsContent.vue lines 96-101, update the
file descriptor logic to return the disabled descriptor with an empty name when
p.fileBase is unset instead of null. Keep the existing disabled forms unchanged
in src/components/window/SkillEditContent.vue lines 34-43,
src/components/window/ProfileEditorContent.vue lines 112-128, and
src/components/window/ThemeEditorContent.vue lines 73-83.
In `@src/services/singleFileCollection.test.ts`:
- Around line 444-549: migrateItems の移行テストに履歴サイドカー追随のケースを追加してください。特に copy-adopt
経路を検証するため、`Bad Name` の主ファイルと `Bad Name.history.json5` を用意して `migrateItems`
を実行し、新しい slug 側へ履歴内容が移送され、旧名側の主ファイルと履歴サイドカーが削除されることを確認してください。
In `@src/stores/columnQueries.test.ts`:
- Around line 54-58: Add a test suite for the store’s recordStoreBaseline method
alongside the applyStoreUpdate tests, verifying it records storeSha512 and
storeVersion while leaving src and updatedAt unchanged. Ensure the test covers
the baseline behavior used by recordQueryBaselines in misstore.ts.
In `@src/stores/columnQueries.ts`:
- Around line 272-294: Update applyStoreUpdate so undefined optional patch
fields do not overwrite existing NamedQueryMeta values when registry entries
omit them. Preserve prev.description and prev.iconUrl unless the patch provides
defined replacements, while continuing to apply other patch fields and persist
the resulting next value.
In `@src/stores/deckProfile.ts`:
- Around line 591-601: The merge predicate currently embedded in the store
should be extracted into a named pure function under src/services/, near the
existing deck profile file logic such as deckProfileFiles. Move the “matches by
id or by name and createdAt” filtering rule into that function, update the store
to call it, and add direct unit tests covering the matching and memory-only
cases.
In `@src/stores/deckProfileFileSync.test.ts`:
- Around line 229-245: The delete test in deckProfileFileSync.test.ts only
verifies file removal; extend it to capture the undo callback returned by
deleteProfile, invoke it immediately, and then wait for and assert that the
profile file exists again. Keep the existing setup and deletion assertion as
appropriate, and cover the saveProfiles/ready continuation outcome.
In `@src/stores/plugins.ts`:
- Around line 611-654: Update applyStoreUpdate so plugin.configData is
synchronized with patch.config: retain values for keys still defined, add
defaults for new keys, and remove entries whose configuration keys were removed.
Preserve existing behavior when patch.config is absent.
In `@src/stores/widgets.ts`:
- Around line 400-428: Update applyStoreUpdate so widget.iconUrl is changed only
when patch.iconUrl is defined, preserving the existing stored icon when the
registry omits it. Keep the remaining metadata assignments and persistence
behavior unchanged, matching installWidget’s conditional icon handling.
🪄 Autofix
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: a9c6a43d-46ab-4c8c-a6d8-2b7483ede113
⛔ Files ignored due to path filters (2)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlsrc-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (62)
package.jsonsrc-tauri/Cargo.tomlsrc-tauri/openapi.jsonsrc-tauri/src/commands/settings.rssrc-tauri/src/settings_store.rssrc-tauri/tauri.conf.jsonsrc/bindings.tssrc/capabilities/builtins/plugins.tssrc/capabilities/builtins/skills.tssrc/capabilities/builtins/theme.tssrc/capabilities/builtins/widgets.tssrc/components/common/AppConfirm.vuesrc/components/common/CodeDiffView.dom.test.tssrc/components/common/CodeDiffView.vuesrc/components/deck/DeckPluginManagerColumn.vuesrc/components/deck/DeckQueryManagerColumn.vuesrc/components/deck/DeckSkillColumn.vuesrc/components/deck/DeckThemeManagerColumn.vuesrc/components/deck/DeckWidgetColumn.vuesrc/components/deck/PluginCard.vuesrc/components/deck/QueryCard.vuesrc/components/deck/ThemeCard.vuesrc/components/deck/WidgetCard.dom.test.tssrc/components/deck/WidgetCard.vuesrc/components/window/BackupContent.vuesrc/components/window/PluginsContent.vuesrc/components/window/ProfileEditorContent.vuesrc/components/window/SkillEditContent.vuesrc/components/window/ThemeEditorContent.vuesrc/services/deckProfileCodec.test.tssrc/services/deckProfileCodec.tssrc/services/deckProfileFiles.tssrc/services/idFreeze.test.tssrc/services/idFreeze.tssrc/services/settingsSlug.test.tssrc/services/settingsSlug.tssrc/services/sidecarFileCollection.test.tssrc/services/sidecarFileCollection.tssrc/services/singleFileCollection.test.tssrc/services/singleFileCollection.tssrc/stores/columnQueries.test.tssrc/stores/columnQueries.tssrc/stores/confirm.tssrc/stores/deck.tssrc/stores/deckProfile.tssrc/stores/deckProfileFileSync.test.tssrc/stores/misstore.test.tssrc/stores/misstore.tssrc/stores/plugins.test.tssrc/stores/plugins.tssrc/stores/skills.test.tssrc/stores/skills.tssrc/stores/skillsFileSync.test.tssrc/stores/theme.dom.test.tssrc/stores/theme.tssrc/stores/themeFileSync.tssrc/stores/widgets.test.tssrc/stores/widgets.tssrc/styles/global.csssrc/theme/types.tssrc/utils/settingsFs.tssrc/utils/toastNotify.ts
| /** 更新の主表示は updatedAt、version は補助 (#1040) */ | ||
| function storeUpdateTitle(entry: StoreSkillEntry): string { | ||
| return `ストア更新日: ${formatDate(entry.updatedAt)} / v${entry.version}` | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check the updatedAt field declaration and the formatDate contract.
rg -nP -C4 'interface Store(Skill|Widget|Theme|Plugin|Query)Entry' src/stores/misstore.ts
rg -nP -C6 'export function formatDate' src/utils/format.tsRepository: notedeck-dev/notedeck
Length of output: 1660
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- StoreSkillEntry declaration ---'
sed -n '165,205p' src/stores/misstore.ts
printf '%s\n' '--- DeckSkillColumn context ---'
sed -n '190,235p' src/components/deck/DeckSkillColumn.vue
printf '%s\n' '--- updatedAt references ---'
rg -n -C3 'updatedAt|StoreSkillEntry|storeUpdateTitle' src
printf '%s\n' '--- registry and API schema references ---'
rg -n -C3 'skill.*registry|registry.*skill|StoreSkill|updated_at|updatedAt' src
printf '%s\n' '--- read-only static verifier ---'
python3 - <<'PY'
from pathlib import Path
import re
misstore = Path("src/stores/misstore.ts").read_text()
deck = Path("src/components/deck/DeckSkillColumn.vue").read_text()
match = re.search(
r"export interface StoreSkillEntry\s*\{(?P<body>.*?)\n\}",
misstore,
re.S,
)
if not match:
raise SystemExit("StoreSkillEntry declaration not found")
body = match.group("body")
field = re.search(r"^\s*updatedAt(\?)?\s*:\s*([^/\n]+)", body, re.M)
print("StoreSkillEntry.updatedAt declaration:", field.group(0).strip() if field else "ABSENT")
print("StoreSkillEntry.updatedAt optional:", bool(field and field.group(1)))
calls = re.findall(r"formatDate\(([^)]*)\)", deck)
print("formatDate arguments in DeckSkillColumn.vue:", calls)
fmt = Path("src/utils/format.ts").read_text()
print("formatDate has falsy-input guard:", bool(re.search(r"if\s*\(\s*!iso\s*\)\s*return\s+['\"]['\"]", fmt)))
PYRepository: notedeck-dev/notedeck
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- misstore registry fetch and skill transformation ---'
sed -n '225,390p' src/stores/misstore.ts
sed -n '470,555p' src/stores/misstore.ts
printf '%s\n' '--- skill registry files ---'
git ls-files | rg -i '(^|/)(registry|registries|misstore)(/|$)|skill.*\.(json|json5|ts)$|registry.*\.(json|json5|ts)$' | head -120
printf '%s\n' '--- bounded registry-related references ---'
rg -n -m 80 -C2 'skillsUrl|fetchSkills|skillEntries|StoreSkillEntry|updatedAt' src/stores/misstore.ts src/components/deck/DeckSkillColumn.vue scripts package.json README.md 2>/dev/null || true
printf '%s\n' '--- read-only boundary verifier ---'
python3 - <<'PY'
from pathlib import Path
import re
text = Path("src/stores/misstore.ts").read_text()
skill = re.search(r"export interface StoreSkillEntry\s*\{(?P<body>.*?)\n\}", text, re.S)
assert skill, "StoreSkillEntry not found"
body = skill.group("body")
print("updatedAt optional in StoreSkillEntry:", bool(re.search(r"^\s*updatedAt\?", body, re.M)))
print("updatedAt required in StoreSkillEntry:", bool(re.search(r"^\s*updatedAt\s*:", body, re.M)))
for m in re.finditer(r"(skillEntries|fetchSkills|skillsUrl|StoreSkillEntry)", text):
line = text.count("\n", 0, m.start()) + 1
print(f"{line}: {text.splitlines()[line-1].strip()}")
PYRepository: notedeck-dev/notedeck
Length of output: 19099
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- registry endpoint declarations ---'
sed -n '1,55p' src/stores/misstore.ts
sed -n '448,468p' src/stores/misstore.ts
printf '%s\n' '--- date behavior probe ---'
node - <<'JS'
function formatDate(iso) {
if (!iso) return ''
return new Date(iso).toLocaleDateString()
}
for (const value of [undefined, null, '', 'not-a-date', '2026-01-01T00:00:00Z']) {
let output
try {
output = formatDate(value)
} catch (error) {
output = `${error.name}: ${error.message}`
}
console.log(JSON.stringify({ value, output }))
}
JSRepository: notedeck-dev/notedeck
Length of output: 2932
Validate registry entries before assigning them to StoreSkillEntry[].
fetchSkills() trusts res.json().skills without runtime validation. Missing updatedAt produces a blank date, and an invalid value produces Invalid Date. Reject or normalize such entries before storeUpdateTitle() uses them.
🤖 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/components/deck/DeckSkillColumn.vue` around lines 218 - 221, Validate or
normalize each entry returned by fetchSkills() before assigning it to
StoreSkillEntry[]. Ensure updatedAt is present and represents a valid date,
rejecting or safely normalizing invalid registry entries so storeUpdateTitle()
never receives missing or invalid date data.
| // Kick off async file sync in background (Tauri only) | ||
| if (settingsFs.isTauri) { | ||
| initFileStorage().catch((e) => | ||
| console.warn('[deckProfile] file storage init failed:', e), | ||
| ) | ||
| initFileStorage() | ||
| .catch((e) => | ||
| console.warn('[deckProfile] file storage init failed:', e), | ||
| ) | ||
| .finally(() => resolveReady?.()) | ||
| } else { | ||
| initialized.value = true | ||
| flushConsoleMigrationNotice() | ||
| resolveReady?.() | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Set initialized when initFileStorage rejects.
initialized.value = true runs only at the end of initFileStorage (line 646). ensureDefaults catches a rejection and resolves ready in .finally, but it never sets initialized.
If profileFiles.loadAll() throws (corrupt directory, permission error, IPC failure), initialized stays false for the whole session in Tauri. Any consumer that gates rendering or actions on initialized then waits forever. The browser branch already sets it unconditionally.
🛡️ Proposed fix
if (settingsFs.isTauri) {
initFileStorage()
.catch((e) =>
console.warn('[deckProfile] file storage init failed:', e),
)
- .finally(() => resolveReady?.())
+ .finally(() => {
+ initialized.value = true
+ resolveReady?.()
+ })
} else {Also applies to: 644-647
🤖 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/stores/deckProfile.ts` around lines 571 - 583, Update the Tauri
initialization flow in ensureDefaults and initFileStorage so initialized.value
is set to true even when initFileStorage rejects, before resolving readiness in
the catch/finally path. Preserve the existing success behavior and browser
branch.
| async function initFileStorage(): Promise<void> { | ||
| const files = await settingsFs.listSkillFiles() | ||
| const fileSkills: SkillMeta[] = [] | ||
| for (const filename of files) { | ||
| try { | ||
| const raw = await settingsFs.readSkillFile(filename) | ||
| const { meta, body } = parseSkillFile(raw) | ||
| const fallbackId = filename.replace(/\.md$/, '') | ||
| fileSkills.push( | ||
| metaFromFrontmatter(meta as SkillFrontmatter, body, fallbackId), | ||
| ) | ||
| } catch (e) { | ||
| console.warn(`[skills] failed to parse ${filename}:`, e) | ||
| } | ||
| } | ||
| const { items: fileSkills } = await skillFiles.loadAll() | ||
| fileSkills.sort((a, b) => a.createdAt - b.createdAt) | ||
|
|
||
| if (fileSkills.length === 0) { | ||
| await seedBuiltIns() | ||
| initialized.value = true | ||
| } else { | ||
| skills.value = fileSkills | ||
| await migrateLegacyAizu() | ||
| await migrateStoreMovedBuiltIns() | ||
| await seedMissingBuiltIns() | ||
| // initialized=true を立てた **後** に sync する。update() 内の persist は | ||
| // `if (initialized.value)` ガード越しなので、立てる前に呼ぶと in-memory | ||
| // だけ反映され disk に書かれず、次回起動も古い frontmatter を読んでしまう。 | ||
| initialized.value = true | ||
| const templates = await loadBuiltInTemplates() | ||
| syncBuiltInsMetadata(templates) | ||
| return | ||
| } | ||
|
|
||
| // 初期化 (この async 関数が走る間) にメモリ追加された skill は残す | ||
| const fileIds = new Set(fileSkills.map((s) => s.id)) | ||
| const memoryOnly = skills.value.filter((s) => !fileIds.has(s.id)) | ||
| skills.value = [...fileSkills, ...memoryOnly] |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Memory-only skills are dropped when the skills directory is empty.
If fileSkills.length === 0, the function calls seedBuiltIns() and returns. seedBuiltIns() assigns skills.value = seeded, so any skill added through add() while initFileStorage() was awaiting is removed from memory and never persisted. The non-empty branch below handles this case with memoryOnly, and src/stores/widgets.ts handles it too. Consider computing memoryOnly before the empty check and appending it after seeding.
🛠️ Proposed fix
async function initFileStorage(): Promise<void> {
const { items: fileSkills } = await skillFiles.loadAll()
fileSkills.sort((a, b) => a.createdAt - b.createdAt)
+ // 初期化中にメモリ追加された skill は残す
+ const fileIds = new Set(fileSkills.map((s) => s.id))
+ const memoryOnly = skills.value.filter((s) => !fileIds.has(s.id))
+
if (fileSkills.length === 0) {
await seedBuiltIns()
+ if (memoryOnly.length > 0) {
+ skills.value = [...skills.value, ...memoryOnly]
+ await Promise.all(memoryOnly.map((s) => persist(s)))
+ }
initialized.value = true
return
}
- // 初期化 (この async 関数が走る間) にメモリ追加された skill は残す
- const fileIds = new Set(fileSkills.map((s) => s.id))
- const memoryOnly = skills.value.filter((s) => !fileIds.has(s.id))
skills.value = [...fileSkills, ...memoryOnly]📝 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.
| async function initFileStorage(): Promise<void> { | |
| const files = await settingsFs.listSkillFiles() | |
| const fileSkills: SkillMeta[] = [] | |
| for (const filename of files) { | |
| try { | |
| const raw = await settingsFs.readSkillFile(filename) | |
| const { meta, body } = parseSkillFile(raw) | |
| const fallbackId = filename.replace(/\.md$/, '') | |
| fileSkills.push( | |
| metaFromFrontmatter(meta as SkillFrontmatter, body, fallbackId), | |
| ) | |
| } catch (e) { | |
| console.warn(`[skills] failed to parse ${filename}:`, e) | |
| } | |
| } | |
| const { items: fileSkills } = await skillFiles.loadAll() | |
| fileSkills.sort((a, b) => a.createdAt - b.createdAt) | |
| if (fileSkills.length === 0) { | |
| await seedBuiltIns() | |
| initialized.value = true | |
| } else { | |
| skills.value = fileSkills | |
| await migrateLegacyAizu() | |
| await migrateStoreMovedBuiltIns() | |
| await seedMissingBuiltIns() | |
| // initialized=true を立てた **後** に sync する。update() 内の persist は | |
| // `if (initialized.value)` ガード越しなので、立てる前に呼ぶと in-memory | |
| // だけ反映され disk に書かれず、次回起動も古い frontmatter を読んでしまう。 | |
| initialized.value = true | |
| const templates = await loadBuiltInTemplates() | |
| syncBuiltInsMetadata(templates) | |
| return | |
| } | |
| // 初期化 (この async 関数が走る間) にメモリ追加された skill は残す | |
| const fileIds = new Set(fileSkills.map((s) => s.id)) | |
| const memoryOnly = skills.value.filter((s) => !fileIds.has(s.id)) | |
| skills.value = [...fileSkills, ...memoryOnly] | |
| async function initFileStorage(): Promise<void> { | |
| const { items: fileSkills } = await skillFiles.loadAll() | |
| fileSkills.sort((a, b) => a.createdAt - b.createdAt) | |
| // 初期化中にメモリ追加された skill は残す | |
| const fileIds = new Set(fileSkills.map((s) => s.id)) | |
| const memoryOnly = skills.value.filter((s) => !fileIds.has(s.id)) | |
| if (fileSkills.length === 0) { | |
| await seedBuiltIns() | |
| if (memoryOnly.length > 0) { | |
| skills.value = [...skills.value, ...memoryOnly] | |
| await Promise.all(memoryOnly.map((s) => persist(s))) | |
| } | |
| initialized.value = true | |
| return | |
| } | |
| skills.value = [...fileSkills, ...memoryOnly] |
🤖 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/stores/skills.ts` around lines 385 - 398, Update initFileStorage so it
captures memoryOnly skills before the fileSkills.length === 0 early return, then
appends those skills to the seeded built-ins after seedBuiltIns completes.
Preserve the existing non-empty merge behavior and ensure skills added during
initialization remain in skills.value.
| function renameTheme(themeId: string, newName: string): void { | ||
| const theme = installedThemes.value.find((t) => t.id === themeId) | ||
| if (!theme) return | ||
|
|
||
| const oldFilename = settingsFs.themeFilename(theme.name || theme.id) | ||
| theme.name = newName | ||
| const newFilename = settingsFs.themeFilename(newName) | ||
|
|
||
| setStorageJson(STORAGE_KEYS.themeInstalledThemes, installedThemes.value) | ||
|
|
||
| if (initialized.value && oldFilename !== newFilename) { | ||
| // Delete old file and write new one (theme id stays the same, only name changes) | ||
| Promise.all([ | ||
| settingsFs.deleteTheme(oldFilename), | ||
| themeFileSync.persistSingleTheme(theme), | ||
| ]).catch((e) => console.warn('[theme] failed to rename theme file:', e)) | ||
| } | ||
| if (!settingsFs.isTauri) return | ||
| // ファイルは rename コマンドで追随させる (ID 不変・主ファイル + 履歴。 | ||
| // 旧削除 + 新書込の並行発火は旧ファイルを孤児化させるため禁止 #913)。 | ||
| // rename の完了を待ってから保存する | ||
| void ready | ||
| .then(async () => { | ||
| adoptMirrorFileBase(theme) | ||
| await themeFileSync.themeFiles.renameItemFiles( | ||
| theme, | ||
| installedThemes.value, | ||
| ) | ||
| await themeFileSync.themeFiles.persistItem(theme, installedThemes.value) | ||
| setStorageJson(STORAGE_KEYS.themeInstalledThemes, installedThemes.value) | ||
| }) | ||
| .catch((e) => console.warn('[theme] failed to rename theme file:', e)) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Replace the renamed theme object before persistence.
Line 471 mutates an item inside installedThemes, which is a shallowRef. Vue does not notify consumers of this nested mutation. The UI can retain the old theme name until another list replacement occurs.
Replace the item in installedThemes.value before writing the mirror and files.
Proposed fix
- theme.name = newName
+ const renamed: MisskeyTheme = { ...theme, name: newName }
+ installedThemes.value = installedThemes.value.map((item) =>
+ item.id === themeId ? renamed : item,
+ )
setStorageJson(STORAGE_KEYS.themeInstalledThemes, installedThemes.value)
if (!settingsFs.isTauri) return
void ready
.then(async () => {
- adoptMirrorFileBase(theme)
+ adoptMirrorFileBase(renamed)
await themeFileSync.themeFiles.renameItemFiles(
- theme,
+ renamed,
installedThemes.value,
)
- await themeFileSync.themeFiles.persistItem(theme, installedThemes.value)
+ await themeFileSync.themeFiles.persistItem(
+ renamed,
+ installedThemes.value,
+ )📝 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.
| function renameTheme(themeId: string, newName: string): void { | |
| const theme = installedThemes.value.find((t) => t.id === themeId) | |
| if (!theme) return | |
| const oldFilename = settingsFs.themeFilename(theme.name || theme.id) | |
| theme.name = newName | |
| const newFilename = settingsFs.themeFilename(newName) | |
| setStorageJson(STORAGE_KEYS.themeInstalledThemes, installedThemes.value) | |
| if (initialized.value && oldFilename !== newFilename) { | |
| // Delete old file and write new one (theme id stays the same, only name changes) | |
| Promise.all([ | |
| settingsFs.deleteTheme(oldFilename), | |
| themeFileSync.persistSingleTheme(theme), | |
| ]).catch((e) => console.warn('[theme] failed to rename theme file:', e)) | |
| } | |
| if (!settingsFs.isTauri) return | |
| // ファイルは rename コマンドで追随させる (ID 不変・主ファイル + 履歴。 | |
| // 旧削除 + 新書込の並行発火は旧ファイルを孤児化させるため禁止 #913)。 | |
| // rename の完了を待ってから保存する | |
| void ready | |
| .then(async () => { | |
| adoptMirrorFileBase(theme) | |
| await themeFileSync.themeFiles.renameItemFiles( | |
| theme, | |
| installedThemes.value, | |
| ) | |
| await themeFileSync.themeFiles.persistItem(theme, installedThemes.value) | |
| setStorageJson(STORAGE_KEYS.themeInstalledThemes, installedThemes.value) | |
| }) | |
| .catch((e) => console.warn('[theme] failed to rename theme file:', e)) | |
| function renameTheme(themeId: string, newName: string): void { | |
| const theme = installedThemes.value.find((t) => t.id === themeId) | |
| if (!theme) return | |
| const renamed: MisskeyTheme = { ...theme, name: newName } | |
| installedThemes.value = installedThemes.value.map((item) => | |
| item.id === themeId ? renamed : item, | |
| ) | |
| setStorageJson(STORAGE_KEYS.themeInstalledThemes, installedThemes.value) | |
| if (!settingsFs.isTauri) return | |
| // ファイルは rename コマンドで追随させる (ID 不変・主ファイル + 履歴。 | |
| // 旧削除 + 新書込の並行発火は旧ファイルを孤児化させるため禁止 #913)。 | |
| // rename の完了を待ってから保存する | |
| void ready | |
| .then(async () => { | |
| adoptMirrorFileBase(renamed) | |
| await themeFileSync.themeFiles.renameItemFiles( | |
| renamed, | |
| installedThemes.value, | |
| ) | |
| await themeFileSync.themeFiles.persistItem( | |
| renamed, | |
| installedThemes.value, | |
| ) | |
| setStorageJson(STORAGE_KEYS.themeInstalledThemes, installedThemes.value) | |
| }) | |
| .catch((e) => console.warn('[theme] failed to rename theme file:', e)) |
🤖 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/stores/theme.ts` around lines 467 - 488, Update renameTheme so it
replaces the renamed theme within installedThemes.value rather than mutating
theme.name in place, ensuring shallowRef consumers observe the change before
setStorageJson and file persistence. Preserve the existing theme lookup, rename
synchronization, and persistence flow.
| import { injectJson5Id } from '@/services/idFreeze' | ||
| import { createSingleFileCollection } from '@/services/singleFileCollection' | ||
| import type { MisskeyTheme, NotedeckThemeMeta } from '@/theme/types' | ||
| import * as settingsFs from '@/utils/settingsFs' | ||
| import { notifyWarningToast } from '@/utils/toastNotify' | ||
|
|
||
| /** | ||
| * テーマ (`themes/<base>.ndtheme.json5` 単一ファイル) の永続化 | ||
| * (#913 で ID → ファイル名対応表化)。 | ||
| * | ||
| * - 対応表の実体は theme オブジェクトの runtime-only な `fileBase`。 | ||
| * ファイルへは書かない (serializeTheme が projection で strip する) | ||
| * - ID 凍結の実効値 = `custom-` + 完全ファイル名 (現行フォールバックと同値) | ||
| * - themes/ の素の `.json5` (規定拡張子でないもの) は従来どおり無視 | ||
| * (drop-in は #1041 スコープ外) | ||
| */ | ||
|
|
||
| type ParsedTheme = Record<string, unknown> | ||
|
|
||
| /** テーマ 1 件のファイル projection。runtime-only の fileBase は含めない。 */ | ||
| function serializeTheme(theme: MisskeyTheme): string { | ||
| const out: Record<string, unknown> = { | ||
| id: theme.id, | ||
| name: theme.name, | ||
| base: theme.base === 'light' ? 'light' : 'dark', | ||
| props: theme.props, | ||
| } | ||
| // NoteDeck 独自メタ ($notedeck) は従来どおりファイルに書く | ||
| if (theme.$notedeck) out.$notedeck = theme.$notedeck | ||
| return JSON5.stringify(out, null, 2) | ||
| } | ||
|
|
||
| export const themeFiles = createSingleFileCollection<MisskeyTheme, ParsedTheme>( | ||
| { | ||
| logTag: 'theme', | ||
| notify: notifyWarningToast, | ||
| kindFallback: 'theme', | ||
| ext: settingsFs.THEME_EXT, | ||
| // 占有判定・sweep には .history.json5 を含む実列挙が要る | ||
| // (規定拡張子の filter はコレクション側が行う) | ||
| list: () => settingsFs.listThemeDirFiles(), | ||
| read: (filename) => settingsFs.readTheme(filename), | ||
| write: (filename, content) => settingsFs.writeTheme(filename, content), | ||
| remove: (filename) => settingsFs.deleteTheme(filename), | ||
| rename: (oldFilename, newFilename) => | ||
| settingsFs.renameTheme(oldFilename, newFilename), | ||
| parse: (raw) => JSON5.parse(raw) as ParsedTheme, | ||
| accepts: (p) => !!p && typeof p === 'object' && !!p.props, | ||
| rawIdOf: (p) => p.id, | ||
| effectiveIdOf: (filename) => `custom-${filename}`, | ||
| injectId: (raw, id) => injectJson5Id(raw, 'id', id), | ||
| fromFile: (p, id, filename) => { | ||
| const theme: MisskeyTheme = { | ||
| id, | ||
| name: typeof p.name === 'string' && p.name ? p.name : filename, | ||
| base: p.base === 'light' ? 'light' : 'dark', | ||
| props: p.props as Record<string, string>, | ||
| } | ||
| // NoteDeck 独自メタ ($notedeck.storeId / installedFor 等) を保持 | ||
| // しないと再起動時にストア紐付き / per-account 紐付きが消える | ||
| if (p.$notedeck && typeof p.$notedeck === 'object') { | ||
| theme.$notedeck = { ...(p.$notedeck as NotedeckThemeMeta) } | ||
| } | ||
| return theme | ||
| }, | ||
| displayNameOf: (p) => (typeof p.name === 'string' ? p.name : ''), | ||
| idOf: (t) => t.id, | ||
| nameOf: (t) => t.name, | ||
| serialize: serializeTheme, | ||
| }, | ||
| ) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Move theme codec and migration rules to src/services/.
The store layer now owns file codec, reconciliation, and migration rules. Keep stores limited to subscription, cache, and UI state. Test the extracted pure logic directly.
src/stores/themeFileSync.ts#L2-L72: move theme parsing, validation, ID injection, and serialization configuration into a service module.src/stores/theme.ts#L686-L725: move memory/file reconciliation and migration planning into a service module, then let the store apply the resulting state and file operations.
As per coding guidelines: “正規化、マイグレーション、マージ規則、ファイル codec などの純ロジックは store に置かず src/services/ に実装し、直接ユニットテストする”.
📍 Affects 2 files
src/stores/themeFileSync.ts#L2-L72(this comment)src/stores/theme.ts#L686-L725
🤖 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/stores/themeFileSync.ts` around lines 2 - 72, Move the theme codec and
migration-related pure logic out of the store layer into service modules: in
src/stores/themeFileSync.ts lines 2-72, relocate parsing, validation, ID
injection, serialization, and the createSingleFileCollection configuration to a
service while preserving its behavior; in src/stores/theme.ts lines 686-725,
relocate memory/file reconciliation and migration planning to a service. Keep
both store locations limited to applying service results alongside subscription,
cache, UI state, and file operations, and expose the extracted pure logic for
direct unit testing.
Source: Coding guidelines
開きデリミタ長を `---\n` (4) 決め打ちにしていたため、CRLF (`---\r\n`) の skill ファイルへ id を凍結すると挿入位置が 1 バイトずれ、id 値と直前の行が 両方壊れていた (`id: 'tenki'l` / `mode: manua`)。Windows の外部エディタで 保存したファイルが対象になる。 開きデリミタをマッチから取り出して長さを導く形に変更し、CRLF の回帰テストを 追加した。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
同じ storeId のプラグインが既にある状態で別スコープへインストールすると、 installPlugin の既存分岐が permissions を確認なしで上書きしていた。 updatePlugin 側は #1040 で「権限拡大は再同意」を実装済みで、この経路だけが 抜け穴になっていた。 確認 + 適用を confirmPluginUpdate に切り出して両経路で共用する。 インストール経路は sha 一致 (中身が同じ) ならスコープのリンクのみ行い本体と 権限に触れず、権限が拡大するときだけ再同意を取る。断られた場合も 「このスコープへ入れる」操作自体は成立させ、中身は既存のまま据え置く。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#913 の不変条件 (ID → 実ファイル名の対応表が唯一の正) を壊す 2 経路を直した。 - renameFileSet: 主ファイルの rename 成功後に履歴サイドカーの rename が 失敗すると例外が上位へ飛び、fileBase が旧名のまま残って実ファイルとずれて いた。履歴の失敗は警告のみで飲む。case-only の 2 段リネームも、中間名へ 移った時点で fileBase を確定させ 2 段目の失敗で取り残されないようにした (singleFileCollection / sidecarFileCollection の両方) - import_group: グループの書込中に失敗すると create_new で予約済みの空 ファイルが解放されないまま error が上位へ飛び、次回 import の衝突判定を 汚していた。失敗時はそのグループの予約を全て解放してから伝播する あわせて、移行後に旧 basename の履歴を新 basename へ移送しない (sweep で 削除する) という #913 の決定を回帰テストで固定した。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- PluginCard: 更新ボタンが capabilityOk を見ておらず、非対応バージョンへ 更新できていた。インストールボタンと同じガードに揃える - ThemeCard: 更新ボタンに disabled が無く連打で並行更新が走っていた - CodeDiffView: エディタ面とたたまれた行の色が直書きだったので --nd-codeEditorBg / --nd-codeEditorFgMuted を global.css に定義して参照する (カスタム CSS から上書き可能。color-mix の挙動は据え置き) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sidebar な widget カラムでは removeWidget が widgetsStore.removeWidget を 呼んでおり、確認なし・undo なしで widget のコードがファイルごと消えていた。 通常カラムでは同じ操作が「配置から外す」だけなので UI から区別できない。 attachWidget が sidebar 並びを「もう 1 つの配置先」として扱っている以上、 外す操作だけが本体を消すのは非対称なので、本体削除ではなく removeFromSidebar による配置解除に揃えた。本体削除はライブラリピッカー (確認ダイアログ + undo トースト) に一本化する。 あわせて removeWidget が元の位置へ戻す復元関数を返すようにし、外す操作にも undo トーストを出す。ボタンのアイコンも破壊的操作と区別できるよう ti-x から ti-circle-minus に変更した。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ゴミ箱 (ti-trash) は本体削除 (コード / 本文 / テーマも消える) 専用にし、 可逆な「配置・スコープから外す」は ti-circle-minus + 中立色に揃えた。 プラグイン / ウィジェット / テーマ / スキル / クエリの 5 種別で同じ意匠・ 同じ語彙 (「〜から外す」/「ライブラリから削除 (…も消えます)」) を使う。 - プラグインのスコープ解除に undo トースト (linkScope で戻す) を足す。 可逆操作なので確認ダイアログは足さない (#747 の方針) - PluginCard の confirmingUninstall は呼び出し側から渡されておらず 2 段階確認として機能していなかったので、prop・スタイル・分岐ごと削除。 emit も意味に合わせて uninstall → detach にリネーム - テーマの per-account カラムは紐付けが自分だけなら本体ごと消えるので、 そのときだけゴミ箱表示に落として実際の動作とアイコンを一致させる Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
アクティブプロファイルの実体は localStorage (STORAGE_KEYS.deckActiveProfile) にあり、settings.json5 には一度も書かれていなかった。宣言だけが残っていて 「ここに載っている」と読めてしまうので消す。 parseSettings は未知キーを forward-compat で保持するため、既存の settings.json5 に値が入っていても壊れない (無視されてそのまま残る)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
主な変更
設定ファイル名と表示名の分離 (#913)
ユーザーが付けた表示名(日本語含む)がそのままファイル名になっていた 5 種別を、「ファイル名は ASCII slug / 参照はファイル内 ID」に分離した。既存ファイルは初回起動時に自動で正規化される(ID は現在値のまま凍結するので、アクティブプロファイルやデッキ内の参照は無追随で整合する)。
配布アイテムの更新検知と更新適用 (#1040)
MisStore からインストールしたテーマ / プラグイン / ウィジェット / クエリ / スキルに更新導線を追加した。
削除操作の見分け (#1048)
同じゴミ箱アイコンが場所によって「配置から外す」と「本体を削除」の別物になっていた問題を整理した。
その他
lruを更新して RUSTSEC-2026-0253 を解消関連
Closes #913
Closes #1040
Closes #1048