Skip to content

Release v1.83.1 - #1220

Merged
hitalin merged 50 commits into
mainfrom
develop
Oct 10, 2026
Merged

hitalin merged 50 commits into
mainfrom
develop

Conversation

@hitalin

@hitalin hitalin commented Oct 10, 2026

Copy link
Copy Markdown
Collaborator

Closes #1165
Closes #1214
Closes #1215
Closes #1216
Closes #1217
Closes #1219

変更一覧

🤖 Generated with Claude Code

hitalin and others added 30 commits October 10, 2026 06:19
v1.83.0 で、同じ画像の同時取得をまとめる処理の待ち側を「先頭が結果を送らずに
終わったら自分で取り直す」に変えた。Windows では絵文字・アバターの画像を
すべて内蔵 HTTP サーバー (同一宛先) から読むため、上流の 429 待ちなどで
取り直しが長引くと WebView の同一宛先の同時接続 (6 本) を塞ぎ、後ろの画像が
全部順番待ちになっていたと見ている (失敗は出ないまま絵文字が出ない)。
Android は custom protocol で読むので起きない。

- 待っていた側は、先頭が取り消されたら以前どおりすぐ失敗を返す (表示側は
  間を置いて取り直し、そのときは片付いた登録から取れる)
- 取り消された取得の登録を片付ける処理と、送り手の消えた登録を見つけたら
  外して取りに行く処理は残す (再起動まで同じ画像が出なくなる不具合は防いだまま)
- リンクプレビュー (OGP) も同じ形に

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ボトムバーのボタンで隣のカラムへ移るとき、ブラウザ標準の smooth スクロールを
使っていて、速さを指定できずノロノロ動いて見えていた。0.2 秒
(--nd-duration-medium) の減速カーブで自前に補間する (体感でおよそ 2 倍速)。
カラム領域の scroll-snap は途中の位置を書くたびに吸着するので、動かしている
間だけ外して着いたら戻す。reduced-motion では瞬時のまま。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
同梱の Twemoji (16.0.1) より新しい絵文字 (U+1FAEA など) は 404 になり、
MkEmoji が文字に戻すものの、仮想スクロールで行が作り直されるたびに取り直して
いた。失敗した URL を上限付きでアプリ全体に覚え、以降は最初から文字で出す。

収録一覧をビルド時に持つ案は、ファイル名だけで約 64KB (gzip 約 10KB) を
起動時のバンドルに足すことになるため採らなかった。失敗の記憶なら 1 種類の
絵文字につき 1 回の 404 で済む。

👁️‍🗨️ は ZWJ 連結なのに同梱側が FE0F を落とした名前で持っており、規則で
作った名前と合わなかった。例外として対応表に置き、ピッカーの全絵文字が
同梱の画像に当たることをテストで照合する。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
確認・入力・ログアウト等のダイアログやポップアップメニューは、CSS の退場アニメ
(--nd-duration-base = 150ms) が終わってからも JS の leaveDuration (200ms) まで
DOM が残り、透明な要素が数十 ms 居座っていた。ドロワーの暗幕のように透明なまま
クリックを吸う面もあった。

leaveDuration を各部品の退場アニメの時間に揃える。シート (200ms) とポップアップ
(150ms) を画面幅で出し分ける部品は、useVaporTransition が閉じ始めるときに
leaveDuration を読むようにして getter で渡す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 最下部へのスクロールと過去分の読み込み後の位置合わせが、ミュートした
  相手の発言を含む一覧の添字で NoteScroller (隠した後の一覧を表示) を
  指していたため、隠した件数だけずれていた。表示している一覧の添字で指す
- ログアウト中の履歴は手元のキャッシュで、既読状態は保存した時点から
  更新されない。読み終えた会話にも点が残るので、ログアウト中は出さない
- 履歴は購読していないため、会話中の新着や送信は取り直すまで並びにも
  プレビューにも出なかった。戻るときに会話の最新メッセージを履歴へ反映し、
  見た会話なので未読の点も消す

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main.ts が読むのは outline 版の tabler-icons.min.css だけで、`-filled` 系の
グリフは入っていない。filled 版の CSS は `.ti` の font-family を上書きするので
併用もできない。名前が無くてもビルドもテストも通り、画面に何も出ないだけなので
気づかれずに残っていた。タスクの編集画面の再生 / ピンのほか、スキルの導入済み
印、チュートリアルの完了印、候補カードの選択印、リスト / クリップのお気に入り、
PiP の最前面ボタン、コマンドパレットの既定タスク実行が該当した。

- `-filled` は outline 版の同じ形に置き換える。状態の切り替えに使っていた
  お気に入り (色で区別済み) はそのまま、PiP の最前面は pin / pinned で区別する
- capability 宣言の icon も Tabler に無い名前だった (ti-megaphone → ti-speakerphone、
  ti-windows → ti-app-window) ので直して再生成
- 使っている名前を CSS と照合する lint (tests/lint/tablerIconNames.test.ts) を
  足し、再発を機械的に止める

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
行に乗ったときに中のボタンが出る、擬似要素の背景が付く、といった子要素側の
hover は遷移が無いか --nd-duration-fast のままで、部品本体の hover
(nd-interactive = --nd-duration-base) と手触りが揃っていなかった。
プロファイルメニューの項目と操作ボタン、ナビバーのアイコン、コードブロックの
操作ボタン、ウィンドウの閉じるボタン (赤への色変化が瞬間だった) を揃える。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tabler-icons の @font-face を font-display: swap で上書きしていたため、読み込み
前は私用領域の文字が代替フォントで描かれ、豆腐 (□) が一瞬見えうった。アイコン
に意味のある代替表示は無いので block にし、読み込みの一瞬だけ不可視にする。
フォントは同梱のサブセットで本番では preload も入るので、不可視の時間は短い。
preload だけでは描画までに読み終わる保証にならないので併用する。幅は従来どおり
:where(.ti) の 1em で先に取っておくので、読み込み後に周りはずれない。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#704 で先読みの既定値を下 20 / 上 5 に下げた結果、パフォーマンスのフェーダーの
省メモリ側の端 (下 15 / 上 5) が既定値とほぼ同じになり、上方向は既定値と一致
していた。フェーダーを下げても先読みがほとんど減らない。端を下 10 / 上 0 に
下げ、既定値との比を以前 (端は既定値の約半分) に戻す。上方向は表示済みの画像が
多いので、省メモリ側では先読みしなくてよい。golden も採取し直す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v1.83.0 でこの移動をブラウザ標準の smooth スクロールにしたため、ノロノロ
動いて見えていた。本家 Misskey のスマホ表示は、ボタンからの画面の切り替えに
スクロールのアニメーションを持たないので、v1.82.0 までと同じく瞬時に切り替える。
指でスワイプして切り替わった分を追いかけない判定はそのまま残す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
アカウント → 操作の 2 段選択で、段の中身と見出しが瞬間で入れ替わっていた。
useVaporTransitionSwitch で退場 (.nd-view-leave) → 登場 (.nd-view-enter) に
する。見出しは親の props から段と同時に変わるので、退場中は前の段の見出しを
残す。開いた直後はシートのスライドだけにし、フェードは段を切り替えたときだけ。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ユーザーのプロフィールは開くたびに users/show を待ってからヒーローを描き、
ノート一覧はその後で mount されて読み込みを始めるので、スピナー → プロフィール
→ ノート一覧の 2 段になっていた。

ユーザーポップアップが持っていた詳細のキャッシュを services/userDetailCache に
出してプロフィールと共有し、取得済み (TTL 内) ならそれで先に描いて取り直した
もので差し替える。ポップアップで見た相手を開くと、ヒーローとノート一覧の
読み込みが同時に始まる。手元の詳細を出せていれば、取り直しの失敗で画面ごと
エラーにはしない。

ポップアップのキャッシュは <script setup> のトップレベルに置かれていて
インスタンスごとに作り直されていたため、実際には一度も効いていなかった。
共有に移したことでこれも効くようになる。上限は boundedCache で守る (#987)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
添付の動画・音声は WebView 標準の controls がそのまま出ていて、OS / WebView
ごとに見た目が違い、テーマにも沿わなかった。本家 MkMediaVideo / MkMediaAudio
と同じ範囲 (再生 / 一時停止、シーク、時間、音量 / ミュート、ループ、再生速度、
ピクチャーインピクチャー、全画面、キー操作) を独自のコントロールで出す。

- 動画はサムネイルがあればそれを画像プロキシ経由のポスターにし、本体は
  preload="none" で再生するまで読まない。タイムラインに動画が並んでも
  相手サーバーへの接続を張らない
- 本体は元の URL のまま。プロキシは Range に答えず、1 ファイルの上限で
  打ち切り、WebView の同一宛先の同時接続を動画が握ると画像が詰まるため
  (理由は mediaProxy.ts の冒頭に残した)
- 状態は要素のイベントから写す composable (useMediaPlayer) にまとめ、
  動画と音声で共有する。毎フレーム currentTime を読まず timeupdate で写す
- タップではコントロールを出すだけにし、マウスは本家と同じくクリックで
  再生 / 一時停止。シークバーのドラッグは外側のスワイプに渡さない
- ドライブのファイル詳細の動画・音声も同じプレイヤーにする

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ライトボックスの画像は元の URL を直接読んでいて、ディスクキャッシュに載らず、
開くたびに相手サーバーへ直接リクエストが出ていた。グリッドと同じプロキシを
無変換 (原寸) で通し、隣の 1 枚の先読みも同じ URL にそろえる。

プロキシで読めないもの (1 ファイルの上限を超える画像など) は何も出ないより
ましなので、その画像だけ元の URL に倒す。

ライトボックスの動画も標準の controls をやめて独自のプレイヤーにする。
プレイヤーは親の幅に合わせて広がるので、外枠を動画の比 (不明なら 16:9) で
画面に収める。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
カードの v-for の key と開閉状態がタスクの ID を鍵にしていたため、ID の入力欄に
1 文字打つたびにカードが作り直され (入力欄のフォーカスも外れる)、開閉状態も
閉じた状態に戻っていた。key と開閉 / 打ちかけの params はタスクのオブジェクトに
結び付けた連番にする。コードタブから読み直したときはオブジェクトが入れ替わるので、
同じ ID のタスクへ開閉と打ちかけを引き継ぐ (これまでの振る舞いを保つ)。

見出しの行はクリックでしか開閉できなかった。行の中にドラッグの取っ手と削除
ボタンがあるので、行ごとボタンにすると入れ子になる。開閉用のボタン
(aria-expanded 付き) を見出しの中に分けて置き、取っ手と削除ボタンはその外に残す。
行の余白のクリックでも開閉できるよう、クリックは引き続き行が受ける。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
本家はエクスポート完了の通知に exportedEntity / fileId を、ルームへの招待の
通知に invitation (ルームを含む) を載せて送ってくるが、RawNotification に
その項目が無く正規化で捨てていた。通知カラムで「何をエクスポートしたか」と
「どのルームに招待されたか」を出せるよう、NormalizedNotification に残す。
招待した人は通知の user で届くので、招待には id とルームだけを持たせる。
他の種類では項目を出さず、既存の JSON の形は変えない。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
エクスポート完了は「エクスポートが完了しました」だけで何のエクスポートか
分からず、出来上がったファイルにも辿れなかった。本家 MkNotification と同じく
見出しを「{x}のエクスポートが完了しました」にし、「ファイルを表示」から
ドライブのファイル詳細ウィンドウを開く (詳細系はウィンドウのモデル #792)。
知らない種類が来たら従来の見出しに戻す。ルームへの招待は本文にルームの
名前を出す。対象の名前は辞書のキーを getter で明示的に引き、死にキー検査に
動的な節を増やさない。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
塗りつぶし版 (-filled) のアイコンは読み込んでいるアイコンフォントに無く、
何も表示されない (#1215 の lint が統合時に検出)。輪郭線の版にする。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
通知カラムは Twemoji の URL を自前で組み立てて <img> にしており、
MkEmoji が持つ「一度読めなかった Twemoji の URL」の記憶 (failedTwemojiUrls, #1219)
を通っていなかった。同梱 Twemoji に無い新しい絵文字は行を作り直すたびに 404 を
繰り返し、失敗時もカスタム絵文字用の再試行 (クエリ付きの取り直し) に回っていた。
Unicode のリアクションは MkEmoji に任せ、他の面と同じく失敗後は文字で出す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
プレビューだけが元の URL を直接読んでいて、WebView から相手サーバーへ
直接接続していた (相手に閲覧の時刻と IP が見え、ディスクキャッシュにも
載らない)。ライトボックスと同じく画像プロキシを通し、プロキシで
読めなかったとき (1 ファイルの上限超え等) だけ元の URL に戻す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renote メニューの登場は 0.2s の spring を直書きしており、退場は CSS が
--nd-duration-fast (100ms) なのに JS の leaveDuration は 200ms で、透明なまま
残る間があった。メニューの役割 (_popup.scss の表) どおり登場は
--nd-duration-medium + --nd-ease-menu、退場は --nd-duration-base + --nd-ease-decel
にし、JS の待ち時間も 150ms に合わせる。

ランチパッドは退場の CSS がデスクトップで --nd-duration-base、モバイルのシートで
--nd-duration-medium なのに、JS は一律 200ms 待ってから更に 220ms の setTimeout で
閉じていた。開いた時点のレイアウトで退場時間を決め、両方の待ちをそれに揃える。

ウィンドウ (DeckWindow の windowIn / windowOut) は既にトークン化済みで、
DeckWindowLayer の待ち時間 (200ms) とも一致していたので変更しない。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	tests/lint/moduleCaches.test.ts
元の URL を WebView から直接 fetch していたため、相手サーバーが CORS を
返さないと取得で落ち、黙って URL のコピーに切り替わっていた。また
JPEG / WebP をそのままクリップボードに書こうとしていて、Chromium は
image/png 以外の write を拒むので、取得できてもここで URL のコピーに
倒れていた。

- 画像プロキシから取り、取れなかったときだけ元の URL を試す
- PNG 以外は canvas で PNG にしてから書く
- 取得は Promise のまま ClipboardItem に渡す (WebKit は await を挟むと
  ユーザー操作の扱いが切れて write を拒むため)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hitalin and others added 18 commits October 10, 2026 08:44
見た目の編集でタスクの ID を他のタスクと同じにしても何も出ず、裏では
コードの検証 (parseTasks の重複検査) が落ちて自動保存が止まっていた。
コードタブを開かないと止まっている理由が分からないので、重複している
ID 欄を FormInput にして error / aria-invalid で欄の直下に出し、
重複している間は保存されないことも文に含める。自動保存の見送りは
別の debounce で走るコードの検証の順序に頼らず、重複を直接見る。

コードタブの「ビジュアルに反映」は誤りがあると disabled だったが、
DEVELOPMENT.md の方針どおり押せるままにし、押されたら誤りの文を
出したままエディタへフォーカスを戻す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HEARTBEAT の知らせはアプリの通知 (受信トレイ) で回収する方針になった。
これまで notify は「デスクトップ通知」が on のときしか流れず、session_id も
無かったので、アプリは受信トレイに残すことも押して開くこともできなかった。
notify は AI が「通知して」とした報告ごとに session_id 付きで流し、設定は
desktop として載せて OS 通知を出すかだけをデバイスに決めさせる。
確認待ちの操作が新しく積まれたことも知れるよう、report に件数を載せる。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
成功・情報で操作の無いものはステータス表示だけで受信トレイに残らないため、
HEARTBEAT の報告のように後から開きたい知らせが流れて消えていた。移る先
(onClick) があるものは軽いものとして扱わず、カードと受信トレイに出す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
notemaid 自身の OS 通知は採用せず、知らせはアプリの通知 (受信トレイ) で回収する
方針になった。報告 (AI が「通知して」としたもの)・確認待ちの操作・自動停止の
知らせを、送り元 HEARTBEAT で受信トレイに必ず残す。押せば報告は該当の AI
セッションを AI カラムで (無ければサイドバーに 1 本開いて)、自動停止は AI 設定の
HEARTBEAT の節を開く。OS 通知は「デスクトップ通知」が on のときだけ。

アプリを閉じている間の報告は、正本の heartbeat セッションから「最後に見た時刻」
より後のものを数え、次に開いたときに 1 件にまとめて出す。受信トレイは保存しない。
最後に見た時刻は端末ごとの事柄なので localStorage に持ち、HEARTBEAT が有効な間
だけ持つ (無効なら閉じている間に報告は来ないので、起動時に全セッションを読まない)。

「アプリを終了しても続ける」「デスクトップ通知」の説明文も実態に合わせる
(常駐の実益は巡回が止まらないこと、閉じている間の報告は次に開いたときに届く)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
notemaid 自身の OS 通知は採用しないことにしたので、ROADMAP の未着手項目を
「採用しない」に理由ごと移し、アプリの通知で回収する仕組みを CLAUDE.md /
DEVELOPMENT.md に書く。サイトの notemaid のページにも、閉じている間の報告は
次に開いたときにアプリの通知へまとめて届くこと (OS 通知は出ないこと) を足し、
en / zh / ko の訳を追従させる。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
右上はサーバーのバッジが使い、右下は目に入りにくい。ナビバーには
積み重ね数のバッジ (左上) が無いので、件数を左上に置いても重ならない。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
赤丸だけでは何件あるか分からない。Windows のオーバーレイは画像
しか渡せないので、3x5 のビットマップ数字を赤円に白で描く
(10 件以上は「9+」)。置き場所は OS が決めるので変えられない。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	src/components/common/MkNote.vue
#	src/components/deck/DeckLaunchPad.vue
ヘッダーの検索ボタンで出し入れする検索バーが瞬間で切り替わっていた。
ボタンから下りてくる面なので、トリガーに付いて開くメニューと同じ
menuEnter / menuLeave (登場 --nd-duration-medium / 退場 --nd-duration-base、
曲線は他のポップアップと同じ) を useVaporTransition で付ける。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
会話を開いている間に別の会話へ来た新着が、履歴一覧の並び・プレビュー・
未読の点に出なかった。履歴一覧は何も購読しておらず、取り直すまで古いまま
だった。

本家は会話のチャンネル (chatUser / chatRoom) を開いていない会話の新着を
main チャンネルの newChatMessage で流す (ChatService の
createMessageToUser / createMessageToRoom。受信から 3 秒たっても既読に
ならなかったものだけ、Detailed の形で)。これを受けるため:

- notecore: main の newChatMessage を ChatMessage に読み直す
  new_chat_message と、main を張るだけの stream_subscribe_main を足す。
  main はアカウント単位の共有チャンネルで外す口が無い (#984) ので、張る
  だけにする
- アプリ: 専用の typed イベント stream-new-chat-message で流す (生封筒は
  観測中しか流れないため)
- フロント: useNewChatMessages が main を張って受け、履歴一覧 (アカウント別・
  全アカウント) に未読として足す。本文に isRead が載らないので未読で足す。
  開いている会話の分は会話に出ているうえ、3 秒前の本文で store を上書きすると
  その間のリアクションを潰すので足さない (戻るときに reflectLatestIntoHistory
  が反映する)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stream_subscribe_main と stream-new-chat-message を足したので、main の寿命が
アカウントセッションに属する理由の列挙に加える。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
画像プロキシは Range に答えず常に 200 で全体を返し、取得をメモリに溜めて
ディスクに書き、1 ファイルの上限で打ち切る。WebKit は Range に答えない
サーバーの動画を再生せず、Chromium も未読の位置へシークできないので、
動画の本体は画像の口に載せられず元の URL を直接読んでいた。

画像とは別の経路を足す。Range / If-Range をそのまま上流へ転送し、
206 / Content-Range / Content-Length / Accept-Ranges を中継するだけで
キャッシュしない (Cache-Control: no-store)。画像側の振る舞いは変えない。

- 守りは画像と同じプロキシトークン (同じ router に載せる)、https 限定と
  host の一次検査、名前解決後の SSRF 検証 (ValidatingResolver)。加えて
  redirect の各 hop も https + host 検査を通ったものだけ追う
- 上流 client は専用に持つ。画像側の共有 client は全体 10 秒の timeout を
  持つが、動画の応答は再生中ずっと続くため。代わりに接続と読み取り間隔を
  区切り、止まった上流に WebView の接続を握らせ続けない
- 上流の失敗は本文を見せずに 502、416 は WebView が範囲を読み直す合図
  なので中継する

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/proxy/media で Range が通るようになったので、動画の本体も元の URL を
WebView から直接読むのをやめる (相手に閲覧の時刻と IP が見えていた)。

宛先の名前は画像の 127.0.0.1 と分けて localhost にする。WebView の
同一宛先の同時接続 (6 本) は host:port の名前単位で数えられ、再生中の
動画は接続を長く握るので、同じ名前だと後ろの画像が全部順番待ちになる。
サーバーは同じで、Host ガードは元から localhost を通す。CSP の media-src、
ATS の例外 (macOS / iOS)、Android の cleartext 例外に localhost を足す。

中継で読めなかったとき (WebView が localhost を許さない環境など) は
ライトボックスの画像と同じく元の URL で 1 度だけ読み直し、それでも
駄目なときだけ従来どおりエラー表示にする。音声の本体はまだ元の URL。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
音声も元の URL を WebView から直接読んでいた。動画と同じく /proxy/media
(Range をそのまま転送、キャッシュしない) に載せ、中継で読めなかったとき
だけ元の URL で 1 度だけ読み直す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

API surface diff

外部アプリ向け API 面 (src-tauri/openapi.json / src/bindings.ts) が変更されています。
互換性への影響 (#709) をレビューしてください。

src-tauri/openapi.json | 95 +++++++++++++++++++++++++++++++++++++++++++++++++-
 src/bindings.ts        | 58 +++++++++++++++++++++++++++---
 2 files changed, 148 insertions(+), 5 deletions(-)
Full diff
diff --git a/src-tauri/openapi.json b/src-tauri/openapi.json
index abb783d..a525a76 100644
--- a/src-tauri/openapi.json
+++ b/src-tauri/openapi.json
@@ -6,7 +6,7 @@
     "license": {
       "name": "MIT"
     },
-    "version": "1.83.0"
+    "version": "1.83.1"
   },
   "paths": {
     "/api": {
@@ -1662,6 +1662,58 @@
           }
         }
       }
+    },
+    "/proxy/media": {
+      "get": {
+        "tags": [
+          "proxy"
+        ],
+        "summary": "動画本体の中継。Range をそのまま上流へ転送し、206 / Content-Range /\nAccept-Ranges を中継する。キャッシュしない (詳細は [`crate::media_stream`])",
+        "operationId": "proxy_media",
+        "parameters": [
+          {
+            "name": "url",
+            "in": "query",
+            "description": "Upstream media URL (https only, public hosts only)",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "t",
+            "in": "query",
+            "description": "起動毎のメディアプロキシトークン (#1099)。`/proxy/image` と同じ値",
+            "required": false,
+            "schema": {
+              "type": [
+                "string",
+                "null"
+              ]
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Whole upstream body (no Range, or upstream ignored it)"
+          },
+          "206": {
+            "description": "Partial content relayed from upstream (Range passthrough)"
+          },
+          "400": {
+            "description": "URL is not https or points to a non-public host"
+          },
+          "403": {
+            "description": "Missing or invalid media proxy token"
+          },
+          "416": {
+            "description": "Range not satisfiable (relayed from upstream)"
+          },
+          "502": {
+            "description": "Upstream fetch failed"
+          }
+        }
+      }
     }
   },
   "components": {
@@ -2411,6 +2463,20 @@
           "createdAt": {
             "type": "string"
           },
+          "exportedEntity": {
+            "type": [
+              "string",
+              "null"
+            ],
+            "description": "Exported entity (for exportCompleted type; antenna / note / following 等)"
+          },
+          "fileId": {
+            "type": [
+              "string",
+              "null"
+            ],
+            "description": "Drive file id of the export result (for exportCompleted type)"
+          },
           "header": {
             "type": [
               "string",
@@ -2428,6 +2494,17 @@
           "id": {
             "type": "string"
           },
+          "invitation": {
+            "oneOf": [
+              {
+                "type": "null"
+              },
+              {
+                "$ref": "#/components/schemas/NotificationChatRoomInvitation",
+                "description": "Chat room invitation (for chatRoomInvitationReceived type)"
+              }
+            ]
+          },
           "message": {
             "type": [
               "string",
@@ -2936,6 +3013,22 @@
         ],
         "description": "Typed body of a Misskey `noteUpdated` streaming event.\nAdjacent tagging (`updateType` + `body`) preserves the historical wire shape\n`{ updateType: \"reacted\", body: { ... } }` so the JSON consumers read is\nunchanged while the contract becomes typed end-to-end."
       },
+      "NotificationChatRoomInvitation": {
+        "type": "object",
+        "description": "通知に載るルームへの招待。招待した人は通知の `user` で届くので持たない",
+        "required": [
+          "id",
+          "room"
+        ],
+        "properties": {
+          "id": {
+            "type": "string"
+          },
+          "room": {
+            "$ref": "#/components/schemas/ChatRoom"
+          }
+        }
+      },
       "ReactionBody": {
         "type": "object",
         "required": [
diff --git a/src/bindings.ts b/src/bindings.ts
index 0a026d0..e067297 100644
--- a/src/bindings.ts
+++ b/src/bindings.ts
@@ -1755,6 +1755,15 @@ async streamConnect(accountId: string) : Promise<Result<null, { code: string; me
 }
 },
 /** @see crates/notecore/src/commands/streaming.rs */
+async streamSubscribeMain(accountId: string) : Promise<Result<null, { code: string; message: string; apiCode: string | null; i18n: JsonValue | null }>> {
+    try {
+    return { status: "ok", data: await TAURI_INVOKE("stream_subscribe_main", { accountId }) };
+} catch (e) {
+    if(e instanceof Error) throw e;
+    else return { status: "error", error: e  as any };
+}
+},
+/** @see crates/notecore/src/commands/streaming.rs */
 async streamDisconnect(accountId: string) : Promise<Result<null, { code: string; message: string; apiCode: string | null; i18n: JsonValue | null }>> {
     try {
     return { status: "ok", data: await TAURI_INVOKE("stream_disconnect", { accountId }) };
@@ -2873,6 +2882,7 @@ streamChatMessageReacted: StreamChatMessageReacted,
 streamChatMessageUnreacted: StreamChatMessageUnreacted,
 streamEmojiChanged: StreamEmojiChanged,
 streamEnvelope: StreamEnvelope,
+streamNewChatMessage: StreamNewChatMessage,
 streamStatus: StreamStatus,
 streamUnread: StreamUnread,
 systemState: SystemState
@@ -2887,6 +2897,7 @@ streamChatMessageReacted: "stream-chat-message-reacted",
 streamChatMessageUnreacted: "stream-chat-message-unreacted",
 streamEmojiChanged: "stream-emoji-changed",
 streamEnvelope: "stream-envelope",
+streamNewChatMessage: "stream-new-chat-message",
 streamStatus: "stream-status",
 streamUnread: "stream-unread",
 systemState: "system-state"
@@ -3426,10 +3437,21 @@ lastPanic: PanicReport | null;
 notemaid: NotemaidDiagnostics }
 /**
  * デバイスへ流す出来事 (flat。Tauri は `nd:ai-heartbeat-event`)。
- * kind: `started` (source) / `finished` (outcome) / `report` (session_id, created) /
- * `titled` (session_id, title) / `notify` (title, body) / `toast` (level, text)
+ * kind: `started` (source) / `finished` (outcome) / `report` (session_id, created, pending) /
+ * `titled` (session_id, title) / `notify` (session_id, title, body, desktop) / `toast` (level, text)
+ * 
+ * `notify` は AI が「通知して」とした報告ごとに流す (アプリの通知の受信トレイに残す, #1165)。
+ * AI 設定の「デスクトップ通知」は `desktop` に載せ、OS 通知を出すかだけをデバイスが決める
+ */
+export type HeartbeatEvent = { kind: string; source?: string | null; outcome?: string | null; sessionId?: string | null; created?: boolean | null; title?: string | null; body?: string | null; 
+/**
+ * `report`: この報告で新しく積まれた確認待ちの操作の数 (0 なら無し)
  */
-export type HeartbeatEvent = { kind: string; source?: string | null; outcome?: string | null; sessionId?: string | null; created?: boolean | null; title?: string | null; body?: string | null; level?: string | null; text?: string | null; 
+pending?: number | null; 
+/**
+ * `notify`: OS のデスクトップ通知も出すか (AI 設定の「デスクトップ通知」)
+ */
+desktop?: boolean | null; level?: string | null; text?: string | null; 
 /**
  * `text` を表示言語で描き直す手がかり (#135)
  */
@@ -3556,7 +3578,19 @@ body?: string | null;
 /**
  * App notification icon URL (for app type; notifications/create の icon)
  */
-icon?: string | null }
+icon?: string | null; 
+/**
+ * Exported entity (for exportCompleted type; antenna / note / following 等)
+ */
+exportedEntity?: string | null; 
+/**
+ * Drive file id of the export result (for exportCompleted type)
+ */
+fileId?: string | null; 
+/**
+ * Chat room invitation (for chatRoomInvitationReceived type)
+ */
+invitation?: NotificationChatRoomInvitation | null }
 export type NormalizedPoll = { choices: NormalizedPollChoice[]; multiple?: boolean; expiresAt: string | null }
 export type NormalizedPollChoice = { text: string; votes?: number; isVoted?: boolean }
 export type NormalizedUser = { id: string; username: string; host: string | null; name: string | null; avatarUrl: string | null; isBot?: boolean; isCat?: boolean; avatarDecorations?: AvatarDecoration[]; emojis?: Partial<{ [key in string]: string }>; instance?: UserInstance | null }
@@ -3651,6 +3685,10 @@ heartbeat: JsonValue;
  * notemaid のログの置き場 (データディレクトリの logs/)
  */
 logDir: string | null }
+/**
+ * 通知に載るルームへの招待。招待した人は通知の `user` で届くので持たない
+ */
+export type NotificationChatRoomInvitation = { id: string; room: ChatRoom }
 /**
  * OS 通知クリック時にフロントへ渡す遷移コンテキスト。
  * noteId があればノート詳細、なければ userId でユーザー詳細を開く。
@@ -4077,6 +4115,18 @@ export type StreamEnvelope = StreamEvent
 export type StreamEvent = { kind: "stream-note"; payload: StreamNoteEvent } | { kind: "stream-notification"; payload: StreamNotificationEvent } | { kind: "stream-mention"; payload: StreamMentionEvent } | { kind: "stream-main-event"; payload: StreamMainEvent } | { kind: "stream-note-updated"; payload: StreamNoteUpdatedEvent } | { kind: "stream-note-capture-updated"; payload: StreamNoteCaptureEvent } | { kind: "stream-chat-message"; payload: StreamChatMessageEvent } | { kind: "stream-chat-message-deleted"; payload: StreamChatMessageDeletedEvent } | { kind: "stream-chat-message-reacted"; payload: StreamChatMessageReactedEvent } | { kind: "stream-chat-message-unreacted"; payload: StreamChatMessageUnreactedEvent } | { kind: "stream-status"; payload: StreamStatusEvent } | { kind: "stream-emoji-changed"; payload: StreamEmojiChangedEvent }
 export type StreamMainEvent = { accountId: string; subscriptionId: string; eventType: string; body: JsonValue }
 export type StreamMentionEvent = { accountId: string; subscriptionId: string; note: NormalizedNote }
+/**
+ * 別の会話に来たチャットの新着 (main の `newChatMessage`)。チャットの履歴一覧が購読する
+ */
+export type StreamNewChatMessage = StreamNewChatMessageEvent
+/**
+ * 別の会話に来たチャットの新着 (main チャンネルの `newChatMessage`)。
+ * 
+ * 本家は受信から 3 秒たっても既読にならなかったメッセージだけをこの名前で流す
+ * (ChatService の createMessageToUser / createMessageToRoom)。会話のチャンネルを
+ * 開いていなくても届くので、チャットの履歴一覧の新着に使う
+ */
+export type StreamNewChatMessageEvent = { accountId: string; message: ChatMessage }
 export type StreamNoteCaptureEvent = ({ updateType: "reacted"; body: NoteReactedBody } | { updateType: "unreacted"; body: NoteUnreactedBody } | { updateType: "pollVoted"; body: NotePollVotedBody } | { updateType: "deleted"; body: NoteDeletedBody }) & { accountId: string; noteId: string }
 export type StreamNoteEvent = { accountId: string; subscriptionId: string; note: NormalizedNote }
 export type StreamNoteUpdatedEvent = 

@github-actions github-actions Bot added 📖Doc Documentation related issue/PR rust Pull requests that update rust code javascript Pull requests that update javascript code labels Oct 10, 2026
@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 116 files, which is 16 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration
  • Configuration used: Repository: notedeck-dev/notedeck/.coderabbit.yml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b9bdfeee-6ad7-419c-bcde-3fff57ba775b

📥 Commits

Reviewing files that changed from the base of the PR and between 549a9ec and c233c8f.


⛔ Files ignored due to path filters (4)
  • Cargo.lock is excluded by !**/*.lock
  • src-tauri/openapi.json is excluded by !src-tauri/openapi.json
  • src/capabilities/declarations.generated.ts is excluded by !**/*.generated.*
  • src/i18n/locale.generated.ts is excluded by !**/*.generated.*

📒 Files selected for processing (116)
  • ARCHITECTURE.md
  • CLAUDE.md
  • CODEMAP.md
  • DEVELOPMENT.md
  • ROADMAP.md
  • SECURITY.md
  • crates/notecli/src/models.rs
  • crates/notecore/capabilities.json5
  • crates/notecore/src/capabilities/generated.rs
  • crates/notecore/src/commands/streaming.rs
  • crates/notecore/src/commands/table.rs
  • crates/notecore/src/http_server.rs
  • crates/notecore/src/image_cache.rs
  • crates/notecore/src/lib.rs
  • crates/notecore/src/media_stream.rs
  • crates/notecore/src/ogp/mod.rs
  • crates/notecore/src/permissions_gate.rs
  • crates/notecore/src/ssrf.rs
  • crates/notecore/src/stream_fanout.rs
  • crates/notemaid/Cargo.toml
  • crates/notemaid/src/heartbeat.rs
  • locales/en-US.source.json
  • locales/en-US.yml
  • locales/ja-JP.yml
  • package.json
  • site/docs/notemaid.md
  • site/en/docs/notemaid.md
  • site/ko/docs/notemaid.md
  • site/zh/docs/notemaid.md
  • src-tauri/Cargo.toml
  • src-tauri/Info.plist
  • src-tauri/android/network_security_config.xml
  • src-tauri/src/commands/utility.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/streaming.rs
  • src-tauri/tauri.conf.json
  • src/adapters/types.ts
  • src/bindings.ts
  • src/capabilities/golden/performance.json
  • src/commands/definitions.ts
  • src/components/common/AccountPickerSheet.vue
  • src/components/common/AiScriptDialog.vue
  • src/components/common/AppConfirm.vue
  • src/components/common/AppPrompt.vue
  • src/components/common/ChoiceCard.vue
  • src/components/common/MkCodeBlock.vue
  • src/components/common/MkDriveFolderSelectDialog.vue
  • src/components/common/MkEmoji.dom.test.ts
  • src/components/common/MkEmoji.vue
  • src/components/common/MkMediaAudio.vue
  • src/components/common/MkMediaGrid.dom.test.ts
  • src/components/common/MkMediaGrid.vue
  • src/components/common/MkMediaLightbox.dom.test.ts
  • src/components/common/MkMediaLightbox.vue
  • src/components/common/MkMediaPlayerMenu.vue
  • src/components/common/MkMediaRange.vue
  • src/components/common/MkMediaVideo.vue
  • src/components/common/MkNote.vue
  • src/components/common/MkUserPopup.vue
  • src/components/common/NoteReactionPickerPopup.vue
  • src/components/common/NoteReactionUsersModal.vue
  • src/components/common/PopupMenu.vue
  • src/components/common/TitleBarMenu.vue
  • src/components/common/notifications/NotificationCenter.vue
  • src/components/deck/DeckChatColumn.vue
  • src/components/deck/DeckLaunchPad.vue
  • src/components/deck/DeckLayout.vue
  • src/components/deck/DeckNavbar.vue
  • src/components/deck/DeckNotificationColumn.vue
  • src/components/deck/DeckProfileMenu.vue
  • src/components/deck/DeckSkillColumn.dom.test.ts
  • src/components/deck/DeckSkillColumn.vue
  • src/components/deck/DeckTaskRunnerColumn.vue
  • src/components/deck/DeckWindow.vue
  • src/components/deck/LogoutDialog.vue
  • src/components/tutorial/TutorialContent.vue
  • src/components/window/AiSettingsContent.vue
  • src/components/window/ClipDetailContent.vue
  • src/components/window/DriveFileDetailContent.dom.test.ts
  • src/components/window/DriveFileDetailContent.vue
  • src/components/window/ListDetailContent.vue
  • src/components/window/TasksEditorContent.dom.test.ts
  • src/components/window/TasksEditorContent.vue
  • src/components/window/TutorialEditorContent.vue
  • src/components/window/UserProfileContent.vue
  • src/components/window/ai-settings/AiHeartbeatSection.vue
  • src/components/window/ai-settings/AiSettingsSection.vue
  • src/composables/useColumnScroll.ts
  • src/composables/useHeartbeatDaemon.ts
  • src/composables/useMediaPlayer.dom.test.ts
  • src/composables/useMediaPlayer.ts
  • src/composables/useNewChatMessages.ts
  • src/composables/useVaporTransition.test.ts
  • src/composables/useVaporTransition.ts
  • src/services/chatHistoryEntries.test.ts
  • src/services/chatHistoryEntries.ts
  • src/services/heartbeatAway.test.ts
  • src/services/heartbeatAway.ts
  • src/services/mediaTime.test.ts
  • src/services/mediaTime.ts
  • src/services/twemoji.test.ts
  • src/services/twemoji.ts
  • src/services/userDetailCache.test.ts
  • src/services/userDetailCache.ts
  • src/stores/deck.ts
  • src/stores/performanceData.ts
  • src/stores/toast.test.ts
  • src/stores/toast.ts
  • src/styles/global.css
  • src/utils/mediaProxy.dom.test.ts
  • src/utils/mediaProxy.ts
  • src/utils/renderedMemo.ts
  • src/utils/storage.ts
  • src/views/PipPage.vue
  • tests/lint/moduleCaches.test.ts
  • tests/lint/tablerIconNames.test.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Deploying notedeck with  Cloudflare Pages  Cloudflare Pages

Latest commit: c233c8f
Status: ✅  Deploy successful!
Preview URL: https://c7e492d4.notedeck-d3a.pages.dev
Branch Preview URL: https://develop.notedeck-d3a.pages.dev

View logs

@claude

claude Bot commented Oct 10, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Comment thread crates/notecore/src/media_stream.rs Dismissed
@hitalin hitalin self-assigned this Oct 10, 2026
hitalin and others added 2 commits October 10, 2026 10:19
名前解決の結果を検査する ValidatingResolver は、名前解決を通らない
IP literal の飛び先 (https://127.0.0.1/ 等) を見ない。公開ホストから
そこへリダイレクトされると内部へ取りに行けてしまっていたので、
動画の中継と同じく hop ごとに validate_external_host を通す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/proxy/image と /proxy/media は上流の Content-Type をそのまま返して
いたので、上流が HTML や SVG を返すと 127.0.0.1:19820 の origin で
script が動き得た。画像・動画・音声以外の型は octet-stream に替え、
CSP の sandbox を付ける。nosniff は Chromium の ORB が型を誤った
上流の画像を通さなくなるので付けない。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hitalin
hitalin merged commit 7c585d0 into main Oct 10, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment