Skip to content

Develop#515

Open
tegnike wants to merge 48 commits into
mainfrom
develop
Open

Develop#515
tegnike wants to merge 48 commits into
mainfrom
develop

Conversation

@tegnike
Copy link
Copy Markdown
Owner

@tegnike tegnike commented Apr 27, 2026

新機能

  • ゲーム実況モードを追加しました。
    • 画面共有映像を定期的にキャプチャし、マルチモーダルAIがゲーム画面をもとに実況コメントを生成・発話します。
    • メイン画面からゲーム実況の開始/停止を切り替えられます。
    • 実況履歴、直近チャットログ、背景分析結果を文脈として利用します。
  • ゲーム実況設定を追加しました。
    • キャプチャ間隔、履歴参照数、プロンプト、画像品質、リサイズ幅、会話ログ保存、映像遅延を設定できます。
  • ゲーム実況の背景分析機能を追加しました。
    • 発話中に補助的な画面解析を行い、次の実況コメントに場面変化を反映できます。
  • 画面共有映像の遅延プレビュー機能を追加しました。
    • ゲーム実況時に映像表示を最大10秒遅延させ、実況音声とのタイミングを合わせやすくしました。
  • 映像表示の表示/非表示切り替えを追加しました。
  • メモリ設定に記憶の復元実行機能を追加しました。
  • キャラクター位置設定を環境変数形式でコピーできる機能を追加しました。
  • Ollamaのreasoningモデル設定に対応しました。
  • ゲーム実況用のプリセットプロンプトとアイコンを追加しました。

改善

  • マルチモーダル設定を簡素化しました。
    • multiModalModeを廃止し、enableMultiModalに統一しました。
    • 画像添付時は、利用可能な場合にそのまま画像付きメッセージとして送信します。
  • AIチャットストリーム生成に中断制御を追加しました。
    • Vercel AI、Dify、OpenAI AudioでAbortSignalに対応しました。
  • ゲーム実況と通常チャットの競合制御を改善しました。
    • 通常チャットを優先し、割り込み後にゲーム実況を再開できるようにしました。
  • ゲーム実況の排他制御を追加しました。
    • Realtime API、音声モード、外部連携、アイドルモード、人感検知、スライドモードとの同時利用を防ぎます。
  • TTS音声合成と発話キューを改善しました。
    • 音声合成の並列化、文分割の調整、セッション単位の停止処理により発話遅延を改善しました。
  • メモリ検索の待ち時間を制限し、チャット応答が長時間ブロックされにくくしました。
  • ローカルLLM利用時のチャット履歴永続化負荷を軽減しました。
  • 画面共有映像・Webcam映像の表示比率制御を改善しました。
  • Node.js 24.x / npm 11.6.2以上を想定したツールチェーンに更新しました。
  • 16言語の翻訳を更新しました。

バグ修正

  • YouTubeモード時にキャプチャ、Webcam、画像追加ボタンが不要に非表示になる問題を修正しました。
  • ゲーム実況の履歴参照数を0に設定できない問題を修正しました。
  • ゲーム実況停止時に再生状態や生成処理が残る問題を修正しました。
  • ゲーム実況と他モードの排他ルールが不足していた問題を修正しました。
  • マルチモーダル設定移行時の型エラーを修正しました。
  • Cloudflareビルド時の型エラーを修正しました。
  • E2Eのnavigator.mediaDevicesモックが既存APIを消してしまう問題を修正しました。
  • 通常E2Eジョブの実行ポートをドキュメントと揃えました。
  • kiosk modeのE2Eで固定待ちに依存していた検証を修正しました。

テスト

  • Playwright E2Eテスト基盤を追加しました。
    • 開発モード、production smoke、mobile smokeを分離して実行できます。
  • チャット、ゲーム実況、キオスク、設定モード、スライド、YouTubeのE2Eテストを追加しました。
  • ゲーム実況、発話キュー、メモリ復元、ストア排他制御などのユニットテストを追加・更新しました。
  • GitHub ActionsにE2E、lint/format、production smokeのジョブを追加・整理しました。

ドキュメント

  • E2EテストのREADMEを追加しました。
  • エージェント向けの作業ガイドAGENTS.mdを追加しました。
  • 翻訳同期、ドキュメント更新、エンドポイント検証のエージェントスキル仕様を追加しました。

その他

  • .env.exampleにゲーム実況関連の環境変数を追加しました。
  • .gitignoretsconfig.jsonを更新しました。
  • Aivis Speechのスピーカー定義を更新しました。
  • public/speakers_aivis.jsonから不要なデフォルトスピーカーエントリを削除しました。

tegnike and others added 30 commits March 23, 2026 16:16
画面キャプチャを一定間隔で自動取得し、マルチモーダルAIに送信して
リアルタイム実況コメントを生成・発話する新機能を実装。

- CaptureServiceシングルトンによる画面キャプチャAPI
- 専用ring bufferによる実況履歴管理(chatLog汚染防止)
- 完了ベースのsetTimeoutループで発話重複を回避
- アイドルモード等との排他制御ルール追加
- 設定画面タブ・環境変数・i18n対応

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
実況コメントをchatLogに保存し、YouTube/Mastraと文脈を自動共有する。
- デフォルトでchatLogに保存するよう変更
- [実況]prefixで実況コメントを区別可能に
- 実況生成時にchatLogの直近メッセージも参照して視聴者コメントの文脈を把握

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
YouTubeモードとゲーム実況モードの同時利用時にキャプチャボタンが表示されない問題を修正。
元の制限は技術的制約ではなく、全モードで常にボタンを表示するように変更。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
YouTubeモードと同じ操作体系に統一。設定ONでボタン表示、ボタン押下で実況開始/停止。
開始時にキャプチャが未開始なら自動で画面共有を開始する。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ハードコードのSUFFIXを廃止し、実況プロンプトをpublic/presets/game-commentary-prompt.txtで管理
- usePresetLoaderに登録し他のプロンプトと同じ仕組みに統一
- キャラクタープロンプトの感情タグ指示と重複しないようプロンプト内容を整理
- gameCommentarySaveToChatを実行時にgetState()で取得しクロージャの古い値参照を修正
- ボタンOFF時にSpeakQueue.stopAll()を呼び進行中の発話を停止
- isRunningRefで最新状態を参照し停止後のタイマー再開を防止

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GameCommentarySettingsはSettingsStateに含まれているため、
Record<string, unknown>へのキャストは不要。Vercelビルドエラーを修正。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Message型は[text, image]の順序で定義されているが、
[image, text]の順になっていたため型エラーが発生していた。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
直列のPromiseチェーンを廃止し、複数文のTTS合成を並列実行するよう変更。
順序管理にpendingResults Mapを導入し、合成完了順に関わらず正しい順序で
SpeakQueueへエンキューする。レート制限を1秒→250msに短縮。
セッション変更時のonCompleteコールバック漏れも修正。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AIが実況セリフとは別に画面の情景描写を生成し、裏でリングバッファに保存
- 次回キャプチャ時に過去の情景描写をコンテキストとして渡し、画面変化の比較や
  ゲーム進行の流れの把握を可能にする
- [scene]タグによる出力フォーマットの解析を追加(後方互換性あり)
- キャプチャ間隔を10-60秒から0-20秒に変更(デフォルト5秒)
- 0秒設定は発話完了後に即座に次のキャプチャを開始(完了ベースsetTimeoutのため安全)
- captureIntervalRefを導入し、実況中の設定変更が即座に反映されるよう修正
- ドキュメント3言語(ja/en/zh)に情景描写の仕様とキャプチャ間隔の変更を反映

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 実況テキストが分割されず一括でTTSに渡されていた問題を修正
- splitSentenceで句点・感嘆符・改行ごとに分割し、文単位で発話キューに投入
- 最後の文の完了時のみ次回キャプチャをスケジュールする動作を維持

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 実況履歴の参照数(contextCount)の最小値を1→0に変更し、履歴参照を無効化可能に
- contextCount取得時の || を ?? に修正(0がfalsyで5に置換される問題)
- contextCount=0時はリングバッファへの追加をスキップ
- 実況プロンプトの説明に[scene]タグの解説を追記
- 実況プロンプトのテキストエリアを縦方向にリサイズ可能に変更

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- serviceReasoningDefaultsにollama用のeffort設定を追加(none/low/medium/high)
- buildReasoningProviderOptionsにollamaのcase分岐を追加

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
マルチモーダル機能の利用モード(always/never/ai-decide)を廃止し、
enableMultiModal(boolean)のみで制御するように簡素化。
既存のmultiModalMode設定はマイグレーションでenableMultiModalに変換される。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VRMモーションタグ(think, cheer, wave, shy等10種)の説明を追加し、
既存・新規の会話例にモーションタグを組み込んだ。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 27, 2026

Claude finished @tegnike's task —— View job


🔍 Comprehensive PR Review

This is a substantial and well-structured PR that introduces significant new functionality while maintaining code quality. Here's my detailed analysis:

🎯 Major Features Added

Game Commentary Mode

Excellent implementation with proper architecture:

  • Strong separation of concerns: gameCommentaryTypes.ts for configuration, generateGameCommentary.ts for AI logic, captureService.ts for screen capture
  • Robust validation: Proper input clamping functions and validation constants
  • Background analysis feature: Smart supplementary scene analysis during speech
  • Good error handling: Proper stream cleanup and AbortController support
  • Comprehensive testing: Full test coverage including edge cases

Playwright E2E Testing Setup

Professional testing infrastructure:

  • Well-configured: Separate development/production test modes
  • Comprehensive coverage: Tests for all major features including game commentary
  • Good CI integration: Proper GitHub Actions workflow setup
  • Helper abstractions: Reusable test helpers in /tests/e2e/helpers/

Agent Skills Documentation

Structured automation documentation:

  • Clear specifications: Well-defined skill boundaries and capabilities
  • Translation synchronization: Automated multi-language support
  • API endpoint verification: Comprehensive endpoint testing framework

🔍 Code Quality Analysis

Strengths

  1. Architecture: Follows existing patterns (singleton services, factory pattern for AI chat)
  2. TypeScript: Comprehensive type definitions and proper error handling
  3. Testing: Excellent test coverage for new features with proper mocking
  4. Performance: Efficient singleton patterns and proper resource cleanup
  5. i18n: Proper internationalization with 16 language support
  6. Configuration: Comprehensive environment variable setup

Security Assessment

  • No hardcoded API keys or secrets
  • Proper environment variable handling
  • Test data uses safe mock values
  • No unsafe DOM manipulation in capture service

Performance Considerations

  • ✅ Good: Singleton pattern for CaptureService prevents memory leaks
  • ✅ Good: Proper stream cleanup in generateGameCommentary
  • ⚠️ Note: Image capture/resize operations could be heavy - good that quality/size limits are configurable

🐛 Issues Found

Minor Issues

  1. File: src/__tests__/features/constants/aiModels.test.ts:44-45

    // Consider using more descriptive test data
    expect(models).toContain('gpt-4o') // Could be fragile if model names change
  2. File: src/features/gameCommentary/captureService.ts:40-56

    // Synchronous image loading could block UI
    const img = new Image()
    img.src = raw
    // Consider using img.onload for async handling

Best Practice Suggestions

  1. Game commentary settings: Consider adding JSDoc comments for complex validation functions
  2. E2E tests: Add viewport/accessibility testing
  3. Translation keys: Some game commentary keys could benefit from more descriptive names

📊 Test Coverage Assessment

Excellent Coverage

  • ✅ Game commentary logic: comprehensive unit tests
  • ✅ E2E workflows: tests for all major user flows
  • ✅ Store exclusion rules: thorough state management testing
  • ✅ Message processing: proper concurrency testing

Coverage Gaps (Minor)

  • Image processing error paths in captureService.ts
  • Some edge cases in background analysis intervals

🚀 Performance & Security

Performance: ⭐⭐⭐⭐⭐

  • Efficient resource management
  • Proper cleanup patterns
  • Reasonable capture intervals and quality settings

Security: ⭐⭐⭐⭐⭐

  • No security vulnerabilities found
  • Proper API key handling
  • Safe image processing without external dependencies

Best Practices: ⭐⭐⭐⭐⭐

  • Follows repository conventions
  • Comprehensive type safety
  • Excellent error handling patterns

🎉 Overall Assessment

This is a high-quality PR that significantly enhances the application while maintaining architectural integrity. The game commentary feature is well-implemented with proper testing, and the E2E testing infrastructure will benefit future development.

Recommendations

  1. Approve and merge - This PR is ready for production
  2. Consider adding performance monitoring for image capture in production
  3. The comprehensive test suite will help maintain quality going forward

Code Quality: ⭐⭐⭐⭐⭐
Feature Implementation: ⭐⭐⭐⭐⭐
Test Coverage: ⭐⭐⭐⭐⭐
Documentation: ⭐⭐⭐⭐⭐


@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

ウォークスルー

ゲーム実況機能の追加、環境変数管理UI、CI/CDの改善(Node 24.x + npm 11.6.2へのピン留め)、新しいPlaywright E2Eテストスイート、14言語にわたる翻訳更新、ドキュメント同期・更新・エンドポイント検証用のエージェントスキル、およびマルチモーダルモードの文字列ベース('never'/'always'/'ai-decide')からブール値(enableMultiModal)への大規模なリファクタリングを実装します。

変更内容

コホート / ファイル 概要
エージェントスキル・ドキュメント
.agents/skills/sync-translations/SKILL.md, .agents/skills/update-docs/SKILL.md, .agents/skills/verify-endpoints/SKILL.md, AGENTS.md
プロジェクト管理向けのAgentコーデックスドキュメント、翻訳同期、ドキュメント更新、エンドポイント検証ワークフローの仕様と開発ガイドラインを追加
ビルド・環境設定
.env.example, package.json, playwright.config.ts, .github/workflows/*, .gitignore, tsconfig.json, scripts/start-e2e-server.js
Node 24.xとnpm 11.6.2へのピン留め、Playwright E2E関連スクリプト・設定、ゲーム実況およびビデオ表示機能用の新しい環境変数、Electronバイナリダウンロードのスキップ設定を追加
ゲーム実況機能(コンポーネント・UI)
src/components/gameCommentaryManager.tsx, src/components/settings/gameCommentarySettings.tsx, src/components/capture.tsx, src/components/common/VideoDisplay.tsx, src/components/menu.tsx, src/components/form.tsx
ゲーム実況設定UI、キャプチャライフサイクル管理、遅延ビデオレンダリング、ゲーム実況トグルを実装
ゲーム実況機能(ロジック・フック)
src/hooks/useGameCommentaryMode.ts, src/features/gameCommentary/generateGameCommentary.ts, src/features/gameCommentary/analyzeGameCommentaryScene.ts, src/features/gameCommentary/captureService.ts, src/features/gameCommentary/gameCommentaryTypes.ts
ゲーム実況スケジューリング、AI生成、背景分析、キャプチャサービスの完全な実装
翻訳ファイル
locales/{ar,de,en,es,fr,hi,it,ja,ko,pl,pt,ru,th,vi,zh-CN,zh-TW}/translation.json
14言語全体で新しいゲーム実況設定、ビデオ表示制御、環境変数コピー機能、デモモード通知の翻訳を追加;既存キーの再配置
マルチモーダルモード リファクタリング
src/components/settings/modelProvider.tsx, src/components/settings/modelProvider/hooks/useAIServiceHandlers.ts, src/components/settings/modelProvider/ModelSelector.tsx, src/components/settings/modelProvider/OpenAIConfig.tsx, src/features/constants/aiModels.ts, src/features/chat/handlers.ts
マルチモーダルモード文字列値('never'/'always'/'ai-decide')をブール値enableMultiModalに置き換え、UIとロジックを簡素化
状態管理・ストア
src/features/stores/settings.ts, src/features/stores/home.ts, src/features/stores/menu.ts, src/features/stores/exclusionRules.ts, src/features/stores/exclusionEngine.ts
ゲーム実況設定、ビデオ表示トグル、新しい排他ルール、永続化デバウンス機構を追加
チャット・AI統合
src/features/chat/aiChatFactory.ts, src/features/chat/difyChat.ts, src/features/chat/openAIAudioChat.ts, src/features/chat/vercelAIChat.ts, src/features/chat/handlers.ts, src/features/messages/speakCharacter.ts, src/features/messages/speakQueue.ts
AbortSignalサポート追加、マルチモーダル画像判定ロジック簡素化、合成結果バッファリング、セッション管理の改善
設定UI コンポーネント
src/components/settings/index.tsx, src/components/settings/idleSettings.tsx, src/components/settings/presenceSettings.tsx, src/components/settings/slide.tsx, src/components/settings/kioskSettings.tsx, src/components/settings/youtube.tsx, src/components/settings/speechInput.tsx, src/components/messageInput.tsx, src/components/toggleSwitch.tsx, src/components/iconButton.tsx, src/components/chatLog.tsx, src/components/slideContent.tsx, src/components/slideControls.tsx, src/components/slides.tsx
ゲーム実況タブ追加、マルチモーダルモード UI削除、排他ルール統合、テスト用のdata-testid属性追加
メモリ・検索
src/features/memory/memoryStoreSync.ts, src/utils/mediaDisplay.ts, src/features/slide/slideAIHelpers.ts, src/lib/api-services/providerOptionsBuilder.ts, src/features/presets/usePresetLoader.ts, src/hooks/useIdleMode.ts, src/hooks/useResizable.ts, src/components/useYoutube.tsx, src/pages/index.tsx
メモリ検索タイムアウト、メディアサイズ計算、プリセットローダーの拡張を追加
ユニットテスト
src/__tests__/components/*, src/__tests__/features/*, src/__tests__/hooks/*
ゲーム実況、メモリ検索、マルチモーダルロジック、チャット流・メモリ永続化、speakQueue/speakCharacter、排他エンジン用の新規・更新テストを追加
Playwright E2E テスト
tests/e2e/*.spec.ts, tests/e2e/helpers/*.ts, tests/e2e/README.md
チャット、ゲーム実況、YouTube/キオスク/スライド/設定/モバイル用の包括的なE2Eテストスイートとヘルパーライブラリを追加
プリセット・設定ファイル
public/presets/game-commentary-prompt.txt, public/presets/game-commentary-background-analysis-prompt.txt, public/presets/preset2.txt
ゲーム実況および背景分析用のAIプロンプトテンプレート、モーションタグドキュメントを追加

推定コードレビュー努力

🎯 4 (複雑) | ⏱️ ~75分

根拠: このPRは複数の領域にまたがる実質的な新機能(ゲーム実況)、破壊的変更(マルチモーダルモードのリファクタリング)、14言語の同一パターン更新、包括的なE2Eテストスイート(400+行)、状態管理の複雑な排他ルール更新を含みます。チェンジセットは異なる関心領域を横断し、密度の高いロジック(speakCharacter、useGameCommentaryMode、generateGameCommentary)を含みます。

関連の可能性がある問題

  • 日本語翻訳キーの使用状況分析 #341: locales/ja/translation.json の新しいGameCommentary/デモキーの追加と既存キーの再配置は、日本語翻訳キー使用状況に直接影響を与えるため関連しています。

関連の可能性がある PR

  • Feature/game commentary mode #514: メインPRと直接関連 — 同じ「ゲーム実況」機能を実装し、同一ファイル(新gameCommentaryフック/コンポーネント/型/サービス、キャプチャサービス、設定/ストア移行、multiModalMode削除に伴うenableMultiModal導入、多数のi18nロケール更新、テスト/Playwright設定)で一致するコードレベルの変更を加えています。
  • 本番リリース #157: src/components/capture.tsx の同じキャプチャコンポーネント実装とキャプチャライフサイクル/クリーンアップロジックの変更により関連しています。
  • fix: ローカルLLM利用時のチャット履歴永続化負荷を軽減する #513: src/features/stores/home.ts で同じ永続化実装をデバウンス/遅延localStorage書き込みモデルに置き換え(pagehide/beforeunloadフラッシュ、コンソールログ削除、fetchガード付き)として、両PRは関連しています。

推奨ラベル

devin, game-commentary, e2e-tests, multimodal-refactor, i18n, ci-cd

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (4)
src/features/stores/home.ts (1)

326-351: 🛠️ Refactor suggestion | 🟠 Major

Promise処理をasync/awaitへ統一してください。

この保存処理だけ.then/.catchチェーンが残っており、同じ関数内の他の非同期処理(11行目のawait addEmbeddingsToMessages)とスタイルが混在しています。コーディングガイドラインに従い、try/catch + await fetchに統一することで一貫性と可読性が向上します。

♻️ 提案差分
-        void fetch('/api/save-chat-log', {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: JSON.stringify({
-            messages: messagesWithEmbedding,
-            isNewFile: shouldCreateNewFile,
-          }),
-        })
-          .then((response) => {
-            if (response.ok) {
-              lastSavedLogLength = state.chatLog.length
-              // 新規ファイルが作成された場合はフラグをリセット
-              shouldCreateNewFile = false
-              console.log(
-                'Messages saved successfully. New saved length:',
-                lastSavedLogLength
-              )
-            } else {
-              console.error('Failed to save chat log:', response.statusText)
-            }
-          })
-          .catch((error) => {
-            console.error('チャットログの保存中にエラーが発生しました:', error)
-          })
+        try {
+          const response = await fetch('/api/save-chat-log', {
+            method: 'POST',
+            headers: {
+              'Content-Type': 'application/json',
+            },
+            body: JSON.stringify({
+              messages: messagesWithEmbedding,
+              isNewFile: shouldCreateNewFile,
+            }),
+          })
+
+          if (response.ok) {
+            lastSavedLogLength = state.chatLog.length
+            // 新規ファイルが作成された場合はフラグをリセット
+            shouldCreateNewFile = false
+            console.log(
+              'Messages saved successfully. New saved length:',
+              lastSavedLogLength
+            )
+          } else {
+            console.error('Failed to save chat log:', response.statusText)
+          }
+        } catch (error) {
+          console.error('チャットログの保存中にエラーが発生しました:', error)
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/stores/home.ts` around lines 326 - 351, Replace the promise
.then/.catch chain for the fetch call that posts messagesWithEmbedding to
'/api/save-chat-log' with an async/await + try/catch block: await the fetch
call, check response.ok and on success set lastSavedLogLength =
state.chatLog.length and shouldCreateNewFile = false (and log success),
otherwise log the response.statusText as an error; catch and log any thrown
error (preserving the existing Japanese error message). This keeps the style
consistent with the earlier await addEmbeddingsToMessages call and uses the same
variables (messagesWithEmbedding, shouldCreateNewFile, lastSavedLogLength,
state.chatLog).
src/__tests__/features/stores/home.test.ts (1)

35-50: ⚠️ Potential issue | 🟡 Minor

global.fetchdelete ではなく元の値へ復元してください。

現在の後片付けだと、元々存在していた fetch を復元できません。元の参照を保持して afterAll で戻す形にしてください。

💡 提案差分
+const originalFetch = global.fetch
+
 describe('homeStore', () => {
   beforeEach(() => {
     jest.spyOn(console, 'log').mockImplementation(() => {})
     jest.spyOn(console, 'error').mockImplementation(() => {})
     ;(global as typeof globalThis & { fetch: jest.Mock }).fetch = jest
       .fn()
       .mockResolvedValue({
         ok: true,
       })
@@
   afterEach(() => {
     jest.restoreAllMocks()
-    delete (global as typeof globalThis & { fetch?: jest.Mock }).fetch
   })
+
+  afterAll(() => {
+    if (originalFetch) {
+      global.fetch = originalFetch
+    } else {
+      delete (global as typeof globalThis & { fetch?: typeof fetch }).fetch
+    }
+  })

Based on learnings テストファイルでグローバルオブジェクト(例:global.fetch)を上書きする場合は、afterAll()フック内で元の値に戻して、テスト間の副作用を防ぐべきである。

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

In `@src/__tests__/features/stores/home.test.ts` around lines 35 - 50, Save the
original global.fetch reference before you mock it and restore that reference in
an afterAll hook instead of deleting it: capture originalFetch (e.g. const
originalFetch = global.fetch) before the mock setup, keep the existing mock in
beforeEach that sets (global as any).fetch = jest.fn(), remove the delete line
in afterEach, call jest.restoreAllMocks() in afterEach as you already do, and
add an afterAll that assigns global.fetch = originalFetch to fully revert the
global state; refer to the existing global.fetch mock, the afterEach block, and
homeStore.setState to locate where to adjust setup/teardown.
src/features/chat/openAIAudioChat.ts (1)

57-80: ⚠️ Potential issue | 🟠 Major

abort を正常終了に変換しないでください。

Line 58 の break だと、その後の flush() / close() に進むため、キャンセルが EOF 扱いになります。1 チャンクでも受信済みだと上位は成功として扱えてしまうので、ここは AbortError を投げて stream を error 終了にしたほうが安全です。

修正例
         for await (const chunk of response) {
-          if (options.signal?.aborted) break
+          if (options.signal?.aborted) {
+            throw new DOMException('The operation was aborted', 'AbortError')
+          }

           const audio = (chunk.choices[0]?.delta as any)?.audio
           if (audio) {
             if (audio.transcript) {
               controller.enqueue(audio.transcript)
@@
           }
         }

+        if (options.signal?.aborted) {
+          throw new DOMException('The operation was aborted', 'AbortError')
+        }
         await bufferManager.flush()
         controller.close()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/chat/openAIAudioChat.ts` around lines 57 - 80, The current
for-await loop in openAIAudioChat.ts checks options.signal?.aborted and does a
plain break, which lets execution continue to await bufferManager.flush() and
controller.close() and thus treats cancel as EOF; instead detect the abort
(options.signal?.aborted) inside the loop and throw an AbortError (or rethrow a
DOMException with name "AbortError") immediately so the stream ends with an
error and flush()/close() are skipped; update the loop surrounding logic that
references response, bufferManager.flush(), and controller.close() accordingly
to rely on the thrown error path.
src/features/stores/exclusionRules.ts (1)

150-177: ⚠️ Potential issue | 🟠 Major

非マルチモーダル切り替えで gameCommentaryEnabled が残ります。

ここは enableMultiModal だけ OFF にしているため、ゲーム実況を有効のまま非対応モデルへ切り替えられます。以降の UI と実行条件がずれるので、この分岐でも gameCommentaryEnabled を落としておいたほうが整合します。
Based on learnings, When adding new mode settings with mutual exclusivity requirements, add corresponding exclusion control rules to /src/features/stores/exclusionRules.ts to maintain consistency

修正例
     apply: () => ({
       conversationContinuityMode: false,
       slideMode: false,
       enableMultiModal: false,
+      gameCommentaryEnabled: false,
     }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/stores/exclusionRules.ts` around lines 150 - 177, The
aiService-nonMultiModal exclusion rule (id: 'aiService-nonMultiModal') currently
turns off conversationContinuityMode, slideMode and enableMultiModal but leaves
gameCommentaryEnabled unchanged; update the rule's apply function to also set
gameCommentaryEnabled: false so switching to a non-multimodal default model
cannot leave game commentary enabled and keeps UI/behavior consistent (modify
the apply return object in the aiService-nonMultiModal rule).
🟠 Major comments (23)
src/hooks/useResizable.ts-185-195 (1)

185-195: ⚠️ Potential issue | 🟠 Major

公開 setSize が制約をバイパスして不正なサイズを保存できます

setSizemin/maxaspectRatio を考慮せずに状態を更新するため、ドラッグ経路では防いでいる無効サイズを外部から注入できます。ここも同じ正規化ロジックを通してください。

修正案(少なくとも clamp を適用)
   const setSize = useCallback((nextSize: { width: number; height: number }) => {
+    const clamp = (value: number, min: number, max: number) =>
+      Math.max(min, Math.min(max, value))
+
+    const clampedWidth = clamp(nextSize.width, minWidth, maxWidth)
+    const clampedHeight = clamp(nextSize.height, minHeight, maxHeight)
+    const normalized = { width: clampedWidth, height: clampedHeight }
+
     setSizeState((currentSize) => {
       if (
-        currentSize.width === nextSize.width &&
-        currentSize.height === nextSize.height
+        currentSize.width === normalized.width &&
+        currentSize.height === normalized.height
       ) {
         return currentSize
       }
-      return nextSize
+      return normalized
     })
-  }, [])
+  }, [minWidth, minHeight, maxWidth, maxHeight])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useResizable.ts` around lines 185 - 195, Public setSize bypasses
the component's min/max/aspectRatio constraints by directly calling setSizeState
with nextSize; update setSize in useResizable.ts to run nextSize through the
same normalization/clamping logic used by the internal resize handlers (e.g.,
call normalizeSize or clampSize before comparing/applying), so it enforces
minWidth/minHeight/maxWidth/maxHeight and aspectRatio constraints before calling
setSizeState; if no helper exists, implement a small clamp+aspect-ratio
normalization and use that inside setSize.
src/hooks/useResizable.ts-87-134 (1)

87-134: ⚠️ Potential issue | 🟠 Major

アスペクト比維持時の境界補正が最終的に制約違反を残す可能性があります

Line 125 以降の再補正で max 側に合わせた後、min 側を再検証していないため、最終サイズが minWidth/minHeight を下回るケースが出ます。制約を満たす幅レンジを先に確定して 1 回で決める形にしたほうが安全です。

修正案(制約レンジを先に確定)
-        const ratio = aspectRatioRef.current || 1
-        const applyFromWidth = (candidateWidth: number) => {
-          const width = clamp(candidateWidth, minWidth, maxWidth)
-          return { width, height: width / ratio }
-        }
-        const applyFromHeight = (candidateHeight: number) => {
-          const height = clamp(candidateHeight, minHeight, maxHeight)
-          return { width: height * ratio, height }
-        }
-
-        const preferHeight = direction === 'top' || direction === 'bottom'
-        const nextSize = preferHeight
-          ? applyFromHeight(newHeight)
-          : applyFromWidth(newWidth)
-
-        newWidth = nextSize.width
-        newHeight = nextSize.height
-
-        if (newWidth > maxWidth) {
-          const adjusted = applyFromWidth(maxWidth)
-          newWidth = adjusted.width
-          newHeight = adjusted.height
-        }
-        if (newHeight > maxHeight) {
-          const adjusted = applyFromHeight(maxHeight)
-          newWidth = adjusted.width
-          newHeight = adjusted.height
-        }
-        if (newWidth < minWidth) {
-          const adjusted = applyFromWidth(minWidth)
-          newWidth = adjusted.width
-          newHeight = adjusted.height
-        }
-        if (newHeight < minHeight) {
-          const adjusted = applyFromHeight(minHeight)
-          newWidth = adjusted.width
-          newHeight = adjusted.height
-        }
-        if (newWidth > maxWidth) {
-          const adjusted = applyFromWidth(maxWidth)
-          newWidth = adjusted.width
-          newHeight = adjusted.height
-        }
-        if (newHeight > maxHeight) {
-          const adjusted = applyFromHeight(maxHeight)
-          newWidth = adjusted.width
-          newHeight = adjusted.height
-        }
+        const ratio = aspectRatioRef.current || 1
+        const preferHeight = direction === 'top' || direction === 'bottom'
+        const candidateWidth = preferHeight ? newHeight * ratio : newWidth
+
+        const lowerWidth = Math.max(minWidth, minHeight * ratio)
+        const upperWidth = Math.min(maxWidth, maxHeight * ratio)
+
+        if (lowerWidth <= upperWidth) {
+          newWidth = clamp(candidateWidth, lowerWidth, upperWidth)
+          newHeight = newWidth / ratio
+        } else {
+          // 制約が矛盾する場合のフォールバック
+          newWidth = clamp(candidateWidth, minWidth, maxWidth)
+          newHeight = clamp(newWidth / ratio, minHeight, maxHeight)
+          newWidth = clamp(newHeight * ratio, minWidth, maxWidth)
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useResizable.ts` around lines 87 - 134, The boundary-correction
loop in useResizable (the block using applyFromWidth/applyFromHeight and
variables newWidth/newHeight, minWidth/maxWidth,minHeight/maxHeight and
preferHeight) can leave the final size violating min constraints because you
reapply max then min then max again; instead compute the valid width range once
and clamp within it in a single step: determine the allowable width interval by
converting minHeight/maxHeight into equivalent width bounds when preferHeight is
true (using applyFromHeight) or converting minWidth/maxWidth into height bounds
when preferHeight is false, then clamp the candidate width to that single
[minAllowedWidth, maxAllowedWidth] range and derive height via
applyFromWidth/applyFromHeight exactly once to set newWidth/newHeight. Ensure
you replace the repeated if-blocks with this single-range clamping logic so
final sizes always respect both min and max constraints.
src/features/memory/memoryStoreSync.ts-79-92 (1)

79-92: ⚠️ Potential issue | 🟠 Major

Promise.race() のタイムアウト後、基盤の searchMemories() と埋め込み取得は中断されません。

Promise.race() で呼び出し元は先に抜けていますが、searchMemories() の処理は継続し、getEmbedding() による /api/embedding への fetch も完了するまで続きます。会話ごとに通る経路なので、遅いバックエンド時にタイムアウトした検索が積み上がると、無駄な API 呼び出しと IndexedDB 処理が蓄積します。

searchMemories()getEmbedding()AbortSignal サポートを追加し、タイムアウト時に検索本体へ中断を伝搬できるようにしてください。例:SearchOptionssignal?: AbortSignal を追加し、fetch 呼び出しに signal を渡すパターン。

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

In `@src/features/memory/memoryStoreSync.ts` around lines 79 - 92, Add AbortSignal
propagation so timed-out searches actually cancel underlying work: update the
SearchOptions type to include signal?: AbortSignal, modify searchMemories(...)
to accept that signal and wire it through any internal calls (including
getEmbedding(...)) so those functions also accept a signal and pass it into
their fetch calls; in memoryStoreSync.ts create an AbortController before
starting searchPromise, use its signal in the search invocation, and on the
Promise.race timeout path call controller.abort() (and still clearTimeout) so
slow backends and getEmbedding fetches are aborted when the timeout resolves.
src/features/messages/speakQueue.ts-84-96 (1)

84-96: ⚠️ Potential issue | 🟠 Major

セッション停止で破棄されたタスクの onComplete が呼ばれていません

stopSession で該当タスクを filter で落とすだけだと、完了待ち側が解放されず詰まる可能性があります。clearQueue(true) と同様に、破棄時に onComplete を呼ぶべきです。

修正案
   const instance = SpeakQueue.getInstance()
-  instance.queue = instance.queue.filter(
-    (task) => task.sessionId !== sessionId
-  )
+  const remainingTasks: SpeakTask[] = []
+  for (const task of instance.queue) {
+    if (task.sessionId === sessionId) {
+      task.onComplete?.()
+      continue
+    }
+    remainingTasks.push(task)
+  }
+  instance.queue = remainingTasks

   if (instance.currentSessionId !== sessionId) {
     return
   }
@@
-  instance.clearQueue()
+  instance.clearQueue(true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/messages/speakQueue.ts` around lines 84 - 96, In stopSession,
you currently drop tasks by filtering instance.queue without invoking their
completion handlers, which leaks waiters; before filtering capture the removed
tasks (e.g., const removed = instance.queue.filter(t => t.sessionId ===
sessionId)), then set instance.queue = instance.queue.filter(t => t.sessionId
!== sessionId) and for each removed task call its onComplete (or equivalent
completion callback) the same way clearQueue(true) does; alternatively call
instance.clearQueue(true) for the removed tasks path—ensure you reference the
existing symbols instance.queue, instance.clearQueue, and the
stopSession/currentSessionId branch so removed tasks are completed before
returning.
.agents/skills/verify-endpoints/SKILL.md-85-96 (1)

85-96: ⚠️ Potential issue | 🟠 Major

Anthropic モデル名を修正が必要です。

テーブル内のモデル名が実際のAnthropicの APIで利用可能なものと異なっています:

  • Line 86: Codex-sonnet-4-5 → 実際のモデルは claude-sonnet-4-6 または claude-opus-4-7 です

Codex- は存在しないプレフィックスです。Anthropicの現在のAPIモデル(2026年4月)では以下を使用してください:

  • claude-opus-4-7 :最も高性能
  • claude-sonnet-4-6 :速度と知能のバランス(推奨)
  • claude-haiku-4-5-20251001 :最速

スキル実行時にモデル名エラーが発生します。

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

In @.agents/skills/verify-endpoints/SKILL.md around lines 85 - 96, Replace the
incorrect Anthropic model name in the SKILL.md table: find the row containing
the provider key `anthropic` and the model string `Codex-sonnet-4-5` and change
it to a valid Anthropic model (e.g., `claude-sonnet-4-6` or `claude-opus-4-7`);
prefer `claude-sonnet-4-6` as the balanced default and update any documentation
or example usage that references `Codex-sonnet-4-5` to the chosen valid model
name.
src/components/settings/index.tsx-234-234 (1)

234-234: ⚠️ Potential issue | 🟠 Major

デスクトップ/モバイルで同一 data-testid が重複しています。

settings-tab-${tab.key} が2箇所で生成されるため、テストロケータが複数一致し不安定になります。識別子を分離してください。

💡 提案差分
- data-testid={`settings-tab-${tab.key}`}
+ data-testid={`settings-tab-${tab.key}-desktop`}
@@
- data-testid={`settings-tab-${tab.key}`}
+ data-testid={`settings-tab-${tab.key}-mobile`}

Also applies to: 296-296

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

In `@src/components/settings/index.tsx` at line 234, The data-testid template
string settings-tab-${tab.key} is generated in two places (causing duplicate
test locators); change each occurrence to a distinct identifier so tests are
stable—e.g., rename one to settings-tab-desktop-${tab.key} and the other to
settings-tab-mobile-${tab.key} or append a mode/index suffix; update both JSX
attributes that currently render data-testid={`settings-tab-${tab.key}`} to use
the unique variants and ensure any tests referencing the old key are updated
accordingly.
tests/e2e/mobile-smoke.mobile.spec.ts-39-43 (1)

39-43: ⚠️ Potential issue | 🟠 Major

evaluate(...click()) ではなく locator.click() に統一して、E2Eテストの安定性を改善してください。

evaluate でのクリックは Playwright の待機・再試行メカニズムをバイパスするため、モバイル環境でのフレークが増加します。

♻️ 置き換え案
-  await page
-    .getByRole('button', { name: /Conversation Log/ })
-    .evaluate((element) => {
-      ;(element as HTMLElement).click()
-    })
+  await page.getByRole('button', { name: /Conversation Log/ }).click()
-  await page.getByTestId('kiosk-mode-toggle').evaluate((element) => {
-    ;(element as HTMLElement).click()
-  })
+  await page.getByTestId('kiosk-mode-toggle').click()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/mobile-smoke.mobile.spec.ts` around lines 39 - 43, Replace the
direct DOM click inside evaluate with Playwright's locator click to restore
built-in waiting/retry: locate the call using page.getByRole('button', { name:
/Conversation Log/ }) (the expression currently followed by .evaluate((element)
=> {(element as HTMLElement).click()})) and change it to await the
locator.click() invocation so the test uses Playwright's stable click behavior
on mobile.
src/components/form.tsx-30-44 (1)

30-44: ⚠️ Potential issue | 🟠 Major

画像取得失敗時に delayedText が残留します。

この変更だと modalImage が来ないケースで pending text がクリアされず、次の別イベントで画像が入ったときに古い送信が発火します。キャプチャ失敗/キャンセル時の clear か、一定時間後のフォールバック送信が必要です。

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

In `@src/components/form.tsx` around lines 30 - 44, The effect leaves delayedText
uncleared when an image never arrives; update the useEffect (which watches
modalImage, delayedText, handleSendChat) to ensure pending text is cleared on
image-failure/cancel: add logic to detect when modalImage is explicitly cleared
or an image-fetch error occurs and call setDelayedText('') (or invoke a
cancellation path), and/or start a fallback timeout (e.g., clear delayedText
after N ms if modalImage stays falsy) so that stale delayedText cannot trigger
handleSendChat later; reference the existing identifiers useEffect, modalImage,
delayedText, handleSendChat, and setDelayedText when implementing the fix.
.agents/skills/sync-translations/SKILL.md-9-88 (1)

9-88: ⚠️ Potential issue | 🟠 Major

このスキル定義は既存の翻訳運用と衝突しています。

ja/translation.json を更新したら他言語も直接書き換える手順になっていて、今回のような非 ja ロケール差分を常態化させます。source of truth を崩すので、ここは locales/ja/translation.json のみを編集対象にする運用へ寄せたほうがよいです。
Based on learnings, Applies to locales/ja/**/*.json : Update only Japanese language files (/locales/ja/) when adding new translation strings; do not manually update other language files

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

In @.agents/skills/sync-translations/SKILL.md around lines 9 - 88, The skill
currently instructs synchronizing missing keys into 15 non-ja locale files (the
"対象言語" table and steps 2–3), which conflicts with the desired source-of-truth
policy; update .agents/skills/sync-translations/SKILL.md to restrict all
operations to Japanese files only (references to locales/ja/translation.json and
the rule "Applies to locales/ja/**/*.json"), remove or rewrite the "対象言語" list
and any steps that add/overwrite keys in other locales, and replace the
merge/translation steps with a single workflow that detects new keys in ja, adds
them only under locales/ja/**/*.json (preserving order/structure/indentation)
and explicitly forbids editing other locale files.
src/features/gameCommentary/analyzeGameCommentaryScene.ts-18-50 (1)

18-50: ⚠️ Potential issue | 🟠 Major

非マルチモーダルモデルだと背景解析が毎回 null になります

ここは毎回 { type: 'image' } を送るのに、送信前の capability check がありません。テキスト専用モデル選択時は getAIChatResponseStream() 側で失敗して、この機能が静かに無効化されます。isMultiModalModel() で事前に弾くか、ゲーム実況の背景解析を自動で無効化した方が安全です。

As per coding guidelines, "src/features/**/*.{ts,tsx}: Implement model capability checking using helper functions like isMultiModalModel(), isReasoningModel(), and isSearchGroundingModel() from the model management system`."

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

In `@src/features/gameCommentary/analyzeGameCommentaryScene.ts` around lines 18 -
50, analyzeGameCommentaryScene is sending an image to getAIChatResponseStream
without checking model capabilities; before building/sending the image message,
obtain the current model (e.g. from your model selection getter) and use
isMultiModalModel(selectedModel) to guard the multimodal path—if the model is
not multimodal, short-circuit (return null or disable background analysis) or
send a text-only fallback; update the code around analyzeGameCommentaryScene and
the call site to perform this check so you never pass a { type: 'image' } to
getAIChatResponseStream when isMultiModalModel() is false.
src/features/stores/settings.ts-886-910 (1)

886-910: ⚠️ Potential issue | 🟠 Major

移行処理が NEXT_PUBLIC_GAME_COMMENTARY_* の初期値を潰しています

mergePersistedSettings()currentState を先に展開しているため、旧 localStorage に存在しない新フィールドは本来そこで env/default が入ります。ここで DEFAULT_GAME_COMMENTARY_CONFIG を書き戻すと、既存ユーザーでは新しい env 初期値が無視されます。移行では legacy key の変換と deprecated key の削除だけに留める方が安全です。

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

In `@src/features/stores/settings.ts` around lines 886 - 910, The migration code
is overwriting environment/default values for the new
NEXT_PUBLIC_GAME_COMMENTARY_* fields by force-writing
DEFAULT_GAME_COMMENTARY_CONFIG into the migrated object; instead, in
mergePersistedSettings() only perform legacy key conversion and removal of
deprecated keys and do not assign DEFAULT_GAME_COMMENTARY_CONFIG values back
into migrated. Remove the blocks that set migrated.gameCommentaryEnabled,
migrated.gameCommentaryPromptTemplate,
migrated.gameCommentaryBackgroundAnalysisPromptTemplate,
migrated.gameCommentaryVideoDelay,
migrated.gameCommentaryBackgroundAnalysisEnabled, and
migrated.gameCommentaryBackgroundAnalysisInterval from the migration logic; if
you must set anything, only map legacy keys to their new names or set values
when an explicit legacy value exists, but do not overwrite values that
currentState (or env NEXT_PUBLIC_GAME_COMMENTARY_*) already provides.
src/features/gameCommentary/captureService.ts-34-55 (1)

34-55: ⚠️ Potential issue | 🟠 Major

img.src 設定直後の寸法参照により、リサイズが機能していません

画像の width/heightonload イベント発火または decode() 完了後のみ利用可能です。Line 43 の判定時点で img.width は 0 のため、常に raw が返され、maxWidthquality による縮小が実質無効です。

img.decode().then(() => { /* 寸法参照と描画 */ }) へ変更するか、呼び出し側で事前に canvas へのリサイズを完了させてください。

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

In `@src/features/gameCommentary/captureService.ts` around lines 34 - 55, The
captureFrame function reads img.width/img.height immediately after setting
img.src so the dimensions are 0 and resizing never runs; make the logic wait for
the image to be decoded (e.g. call await img.decode() or attach img.onload and
proceed in its callback) before checking img.width and drawing to canvas, update
captureFrame (and its callers) to return a Promise<string|null> if you use
async/await, and ensure error handling still returns raw on decode/draw failures
while using the same variables (captureFrame, img, canvas, ctx) so the
resize/path and quality (quality ?? 0.7) are applied correctly.
src/components/capture.tsx-127-229 (1)

127-229: ⚠️ Potential issue | 🟠 Major

遅延描画 effect のメモリリークと async/await 違反を修正してください

現在のコードには 2 つの問題があります:

  1. Coding guideline 違反: createImageBitmap(offscreen).then(...).then() を使用していますが、**/*.{ts,tsx,js,jsx} では async/await 構文を使う必要があります。

  2. Race condition: effect の cleanup 後も createImageBitmap() の Promise が解決し、既にクリアされた frameBuffer へ bitmap を追加しようとします。このため ImageBitmap が適切に close されず、メモリリークが発生します。

修正として以下を実装してください:

  • captureStatus を selector で読み込んで effect の起動条件に追加し、キャプチャが非アクティブな状況での不要な RAF ループを防止
  • disposed フラグを使用して cleanup 後に解決した Promise 内で bitmap を即座に close
  • .then()async/await で書き換え
修正イメージ
+  const captureStatus = homeStore((s) => s.captureStatus)
+
   useEffect(() => {
-    if (!isVideoDelayed || videoDelaySeconds <= 0) return
+    if (!captureStatus || !isVideoDelayed || videoDelaySeconds <= 0) return

     const CAPTURE_FPS = 15
     const CAPTURE_INTERVAL_MS = 1000 / CAPTURE_FPS

     const frameBuffer: { bitmap: ImageBitmap; time: number }[] = []
     const offscreen = document.createElement('canvas')
     const offCtx = offscreen.getContext('2d')
     let animId = 0
     let lastCapture = 0
+    let disposed = false

     const loop = () => {
       const video = videoRef.current
       const canvas = delayCanvasRef.current

       if (video && canvas && video.readyState >= 2 && offCtx) {
         const now = performance.now()

         if (now - lastCapture >= CAPTURE_INTERVAL_MS) {
           lastCapture = now
           const captureTime = now

           const aspect = video.videoWidth / video.videoHeight
           const bufW = bufferWidth
           const bufH = Math.round(bufferWidth / (aspect || 1))

           if (offscreen.width !== bufW) offscreen.width = bufW
           if (offscreen.height !== bufH) offscreen.height = bufH

           offCtx.drawImage(video, 0, 0, bufW, bufH)

-          createImageBitmap(offscreen).then((bitmap) => {
-            frameBuffer.push({ bitmap, time: captureTime })
-
-            const cutoff = performance.now() - (videoDelaySeconds + 2) * 1000
-            while (frameBuffer.length > 0 && frameBuffer[0].time < cutoff) {
-              frameBuffer[0].bitmap.close()
-              frameBuffer.shift()
-            }
-          })
+          void (async () => {
+            const bitmap = await createImageBitmap(offscreen)
+            if (disposed) {
+              bitmap.close()
+              return
+            }
+
+            frameBuffer.push({ bitmap, time: captureTime })
+
+            const cutoff = performance.now() - (videoDelaySeconds + 2) * 1000
+            while (frameBuffer.length > 0 && frameBuffer[0].time < cutoff) {
+              frameBuffer[0].bitmap.close()
+              frameBuffer.shift()
+            }
+          })()
         }

         const targetTime = performance.now() - videoDelaySeconds * 1000
         let frameToShow: ImageBitmap | null = null
         ...
       }

       animId = requestAnimationFrame(loop)
     }

     animId = requestAnimationFrame(loop)

     return () => {
+      disposed = true
       cancelAnimationFrame(animId)
       for (const frame of frameBuffer) {
         frame.bitmap.close()
       }
       frameBuffer.length = 0
       currentDelayedFrameRef.current = null
     }
-  }, [isVideoDelayed, videoDelaySeconds, bufferWidth])
+  }, [captureStatus, isVideoDelayed, videoDelaySeconds, bufferWidth])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/capture.tsx` around lines 127 - 229, Effect may leak
ImageBitmaps and uses .then(); update the effect to also read captureStatus from
the settingsStore selector and bail out if capture is inactive, replace
createImageBitmap(...).then(...) with an async/await flow inside the loop (use
an async helper or make loop async) and introduce a disposed boolean that is set
in the cleanup to detect when the Promise resolves after unmount; when disposed
is true, immediately call bitmap.close() instead of pushing into frameBuffer.
Ensure you still close all bitmaps from frameBuffer in cleanup and update
references: videoRef, delayCanvasRef, frameBuffer, currentDelayedFrameRef,
createImageBitmap, and the loop/animId handling.
locales/ru/translation.json-96-97 (1)

96-97: ⚠️ Potential issue | 🟠 Major

このリポジトリ運用では非 ja ロケールの手動追加は避けるべきです。

新規翻訳キー追加時は /locales/ja/ のみ更新し、他言語は別フローで同期する前提なので、このファイルの追加分は運用ルールから外れています。

Based on learnings, "Update only Japanese language files (/locales/ja/) when adding new translation strings; do not manually update other language files".

Also applies to: 308-308, 435-436, 524-524, 631-691

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

In `@locales/ru/translation.json` around lines 96 - 97, This change manually adds
new translation keys ("CopyEnvVars" and "CopyEnvVarsInfo") to a non-Japanese
locale (ru/translation.json) which violates the repo rule to only update
/locales/ja/ when adding new strings; revert the additions in
locales/ru/translation.json (remove the "CopyEnvVars" and "CopyEnvVarsInfo"
entries) and ensure the new keys are only added to the /locales/ja/ files so
that other locales are synchronized via the designated localization flow; apply
the same rollback to the other ranges referenced (lines around the additional
ranges) where non-ja locales were updated.
locales/de/translation.json-96-97 (1)

96-97: ⚠️ Potential issue | 🟠 Major

このリポジトリ運用では非 ja ロケールの手動追加は避けるべきです。

新規翻訳キー追加時は /locales/ja/ のみ更新し、他言語は別フローで同期する前提なので、このファイルの追加分は運用ルールから外れています。

Based on learnings, "Update only Japanese language files (/locales/ja/) when adding new translation strings; do not manually update other language files".

Also applies to: 308-308, 435-436, 524-524, 631-691

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

In `@locales/de/translation.json` around lines 96 - 97, The change added new
German translation keys ("CopyEnvVars" and "CopyEnvVarsInfo") which violates the
rule to only update Japanese locale files; remove the newly added keys from
locales/de/translation.json so non-ja locales are not manually edited, and
ensure the new strings exist only under /locales/ja/ (e.g., in the corresponding
ja translation file); repeat this correction for the other mentioned additions
(the entries referenced around lines 308, 435-436, 524, and 631-691) to keep
only /locales/ja/ updated and leave other locale files untouched.
locales/es/translation.json-96-97 (1)

96-97: ⚠️ Potential issue | 🟠 Major

このリポジトリ運用では非 ja ロケールの手動追加は避けるべきです。

新規翻訳キー追加時は /locales/ja/ のみ更新し、他言語は別フローで同期する前提なので、このファイルの追加分は運用ルールから外れています。

Based on learnings, "Update only Japanese language files (/locales/ja/) when adding new translation strings; do not manually update other language files".

Also applies to: 308-308, 435-436, 524-524, 631-691

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

In `@locales/es/translation.json` around lines 96 - 97, Remove the manually added
non-Japanese translation entries for the keys "CopyEnvVars" and
"CopyEnvVarsInfo" (and the other similar additions flagged at the review) from
all non-ja locale files and instead add the new keys and their translations only
in the Japanese locale file (locale key definitions should be placed under the
ja translations); revert the edits in locales like es/translation.json so they
do not contain these new keys and ensure the canonical additions live only in
the /locales/ja/ translation file where new strings are introduced.
src/components/common/VideoDisplay.tsx-121-162 (1)

121-162: ⚠️ Potential issue | 🟠 Major

遅延背景の再描画が常時 60fps で回っています。

Line 128 以降は delayedFrameRef.current が更新されていなくても、全画面 canvas を毎フレーム描き直します。実況モードや kiosk の常時表示では CPU/GPU を無駄に使い続けるので、少なくとも「フレーム参照が変わった時」か「viewport サイズが変わった時」だけ描画する形に絞った方がよいです。

locales/zh-TW/translation.json-96-97 (1)

96-97: ⚠️ Potential issue | 🟠 Major

このリポジトリ運用では非 ja ロケールの手動追加は避けるべきです。

新規翻訳キー追加時は /locales/ja/ のみ更新し、他言語は別フローで同期する前提なので、このファイルの追加分は運用ルールから外れています。

Based on learnings, "Update only Japanese language files (/locales/ja/) when adding new translation strings; do not manually update other language files".

Also applies to: 308-308, 435-436, 524-524, 631-691

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

In `@locales/zh-TW/translation.json` around lines 96 - 97, The PR added new
translation keys ("CopyEnvVars" and "CopyEnvVarsInfo") directly into
non-Japanese locale files which violates the repo rule to update only
/locales/ja/ when adding strings; revert the manual additions in
locales/zh-TW/translation.json (and any other non-ja files referenced: ranges
around 308, 435-436, 524, 631-691) so those files no longer contain
"CopyEnvVars" / "CopyEnvVarsInfo" (or other newly added keys), and ensure the
new keys are added only to /locales/ja/translation.json so the downstream
translation sync flow can propagate them to other locales.
src/features/gameCommentary/gameCommentaryTypes.ts-64-95 (1)

64-95: ⚠️ Potential issue | 🟠 Major

NaN が clamp をすり抜けます。

Line 65 以降は NaN の比較結果がすべて false なので、入力欄を空にしたり不正値を渡したときにそのまま NaN を返します。これが保存やタイマー計算に入ると、実況スケジュールや UI 表示が壊れます。Number.isFinite を先に弾く必要があります。

修正案
+function clampNumber(
+  value: number,
+  min: number,
+  max: number,
+  fallback: number
+): number {
+  if (!Number.isFinite(value)) return fallback
+  return Math.min(max, Math.max(min, value))
+}
+
 export function clampCaptureInterval(value: number): number {
-  if (value < GAME_COMMENTARY_INTERVAL.MIN) return GAME_COMMENTARY_INTERVAL.MIN
-  if (value > GAME_COMMENTARY_INTERVAL.MAX) return GAME_COMMENTARY_INTERVAL.MAX
-  return value
+  return clampNumber(
+    value,
+    GAME_COMMENTARY_INTERVAL.MIN,
+    GAME_COMMENTARY_INTERVAL.MAX,
+    DEFAULT_GAME_COMMENTARY_CONFIG.gameCommentaryCaptureInterval
+  )
 }
 
 export function clampContextCount(value: number): number {
-  if (value < GAME_COMMENTARY_CONTEXT_COUNT.MIN)
-    return GAME_COMMENTARY_CONTEXT_COUNT.MIN
-  if (value > GAME_COMMENTARY_CONTEXT_COUNT.MAX)
-    return GAME_COMMENTARY_CONTEXT_COUNT.MAX
-  return value
+  return clampNumber(
+    value,
+    GAME_COMMENTARY_CONTEXT_COUNT.MIN,
+    GAME_COMMENTARY_CONTEXT_COUNT.MAX,
+    DEFAULT_GAME_COMMENTARY_CONFIG.gameCommentaryContextCount
+  )
 }
 
 export function clampVideoDelay(value: number): number {
-  if (value < GAME_COMMENTARY_VIDEO_DELAY.MIN)
-    return GAME_COMMENTARY_VIDEO_DELAY.MIN
-  if (value > GAME_COMMENTARY_VIDEO_DELAY.MAX)
-    return GAME_COMMENTARY_VIDEO_DELAY.MAX
-  return value
+  return clampNumber(
+    value,
+    GAME_COMMENTARY_VIDEO_DELAY.MIN,
+    GAME_COMMENTARY_VIDEO_DELAY.MAX,
+    DEFAULT_GAME_COMMENTARY_CONFIG.gameCommentaryVideoDelay
+  )
 }
 
 export function clampBackgroundAnalysisInterval(value: number): number {
-  if (value < GAME_COMMENTARY_BACKGROUND_ANALYSIS_INTERVAL.MIN)
-    return GAME_COMMENTARY_BACKGROUND_ANALYSIS_INTERVAL.MIN
-  if (value > GAME_COMMENTARY_BACKGROUND_ANALYSIS_INTERVAL.MAX)
-    return GAME_COMMENTARY_BACKGROUND_ANALYSIS_INTERVAL.MAX
-  return value
+  return clampNumber(
+    value,
+    GAME_COMMENTARY_BACKGROUND_ANALYSIS_INTERVAL.MIN,
+    GAME_COMMENTARY_BACKGROUND_ANALYSIS_INTERVAL.MAX,
+    DEFAULT_GAME_COMMENTARY_CONFIG.gameCommentaryBackgroundAnalysisInterval
+  )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/gameCommentary/gameCommentaryTypes.ts` around lines 64 - 95,
Each clamp function (clampCaptureInterval, clampContextCount, clampVideoDelay,
clampBackgroundAnalysisInterval) currently lets NaN pass because comparisons
with NaN are false; at the top of each function add a guard using
Number.isFinite(value) and if it returns false return the corresponding
GAME_COMMENTARY_* .MIN value so invalid/empty inputs produce a safe minimum
rather than NaN.
locales/ar/translation.json-96-97 (1)

96-97: ⚠️ Potential issue | 🟠 Major

この翻訳追加はリポジトリの運用ルールとずれています。

新規キー追加は /locales/ja/ だけを手で更新し、他言語は翻訳フローに任せる前提でした。このファイルを直接更新すると、以後の自動同期や再生成と競合しやすいです。日本語ソース側だけを更新して、このファイルの差分は外した方がよいです。

Based on learnings: "Update only Japanese language files (/locales/ja/) when adding new translation strings; do not manually update other language files".

Also applies to: 308-308, 435-436, 524-524, 631-691

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

In `@locales/ar/translation.json` around lines 96 - 97, This change adds new
translation keys (e.g., "CopyEnvVars" and "CopyEnvVarsInfo") directly into
non-Japanese locale files which violates the repo rule to only edit /locales/ja/
for new strings; revert/remove these added keys from locales/ar/translation.json
(and the other listed non-ja files/lines: 308, 435-436, 524, 631-691) so only
the Japanese source (/locales/ja/) contains the new keys, then leave other
language files to be updated by the translation sync/flow.
locales/hi/translation.json-96-97 (1)

96-97: ⚠️ Potential issue | 🟠 Major

この翻訳追加はリポジトリの運用ルールとずれています。

新規キー追加は /locales/ja/ だけを手で更新し、他言語は翻訳フローに任せる前提でした。このファイルを直接更新すると、以後の自動同期や再生成と競合しやすいです。日本語ソース側だけを更新して、このファイルの差分は外した方がよいです。

Based on learnings: "Update only Japanese language files (/locales/ja/) when adding new translation strings; do not manually update other language files".

Also applies to: 308-308, 435-436, 524-524, 631-691

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

In `@locales/hi/translation.json` around lines 96 - 97, The hi translation file
was manually updated with new keys ("CopyEnvVars", "CopyEnvVarsInfo" and the
other added keys referenced) which conflicts with the repo rule to only add new
strings in Japanese; revert/remove these newly added keys from
locales/hi/translation.json so the file matches the automated translation
source, ensure only the Japanese source under locales/ja/ contains the new
keys/strings, and leave other language files to the translation flow; update the
PR to include only the ja changes and exclude the hi modifications.
locales/ko/translation.json-96-97 (1)

96-97: ⚠️ Potential issue | 🟠 Major

この翻訳追加はリポジトリの運用ルールとずれています。

新規キー追加は /locales/ja/ だけを手で更新し、他言語は翻訳フローに任せる前提でした。このファイルを直接更新すると、以後の自動同期や再生成と競合しやすいです。日本語ソース側だけを更新して、このファイルの差分は外した方がよいです。

Based on learnings: "Update only Japanese language files (/locales/ja/) when adding new translation strings; do not manually update other language files".

Also applies to: 308-308, 435-436, 524-524, 631-691

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

In `@locales/ko/translation.json` around lines 96 - 97, You added new translation
keys ("CopyEnvVars" and "CopyEnvVarsInfo") directly into a non-Japanese locale,
which breaks the repo rule that only /locales/ja/ should be edited for new
strings; revert/remove these keys from the Korean translation file (and any
other non-ja files you edited) so only the Japanese source contains the new
entries, and leave other languages to the automated translation/sync flow;
locate the keys by name ("CopyEnvVars", "CopyEnvVarsInfo") and remove their
additions (or revert the file changes) while keeping the new keys only in the
Japanese locale.
src/hooks/useGameCommentaryMode.ts-373-382 (1)

373-382: ⚠️ Potential issue | 🟠 Major

maxPastMessages = 0 で履歴が 0 件になっていません。

slice(maxPastMessages > 0 ? -maxPastMessages : 0) は、0 のとき slice(0) になって chatLog 全件を送ってしまいます。履歴を送らない設定で逆にプロンプトが最大化されるので、0 以下は [] に分岐した方が安全です。

💡 修正案
-      const recentMessages = chatLog
-        .filter((m) => m.role === 'user' || m.role === 'assistant')
-        .slice(maxPastMessages > 0 ? -maxPastMessages : 0)
-        .map((m) => ({
-          role: m.role,
-          content: typeof m.content === 'string' ? m.content : '',
-        }))
+      const recentMessages =
+        maxPastMessages > 0
+          ? chatLog
+              .filter((m) => m.role === 'user' || m.role === 'assistant')
+              .slice(-maxPastMessages)
+              .map((m) => ({
+                role: m.role,
+                content: typeof m.content === 'string' ? m.content : '',
+              }))
+          : []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useGameCommentaryMode.ts` around lines 373 - 382, The current logic
for recentMessages uses slice(maxPastMessages > 0 ? -maxPastMessages : 0) which
causes maxPastMessages === 0 to return the entire chatLog; change it so that
when settingsStore.getState().maxPastMessages is less than or equal to 0 you set
recentMessages to an empty array, otherwise filter homeStore.getState().chatLog
for roles 'user'/'assistant' and slice(-maxPastMessages) then map to {role,
content} as before (update the recentMessages assignment to branch on
maxPastMessages <= 0).
🟡 Minor comments (11)
src/utils/mediaDisplay.ts-7-13 (1)

7-13: ⚠️ Potential issue | 🟡 Minor

不正な上限値で 0/負のサイズを返す可能性があります

maxWidth または maxHeight が 0 以下でもその値を返してしまうため、下流に無効な寸法が伝播します。ここは最小値に正規化するか、明示的にエラーに寄せるのが安全です。

💡 修正案(例: 上限値不正時は最小サイズへフォールバック)
   if (
-    sourceWidth <= 0 ||
-    sourceHeight <= 0 ||
     maxWidth <= 0 ||
     maxHeight <= 0
   ) {
+    return { width: 1, height: 1 }
+  }
+
+  if (sourceWidth <= 0 || sourceHeight <= 0) {
     return { width: maxWidth, height: maxHeight }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/mediaDisplay.ts` around lines 7 - 13, The current guard returns raw
maxWidth/maxHeight when any input is <= 0, which can propagate invalid
dimensions; update the early-return in src/utils/mediaDisplay.ts (the block
checking sourceWidth, sourceHeight, maxWidth, maxHeight) to normalize bad max
values by falling back to a guaranteed minimum dimension (e.g., a MIN_DIMENSION
constant or 1) before returning, i.e. compute normalizedWidth = max(maxWidth,
MIN_DIMENSION) and normalizedHeight = max(maxHeight, MIN_DIMENSION) and return
those normalized values instead of the raw maxWidth/maxHeight; ensure the chosen
MIN_DIMENSION is clearly named and documented and used wherever this function
can return fallback sizes.
AGENTS.md-57-57 (1)

57-57: ⚠️ Potential issue | 🟡 Minor

コードフェンスに言語指定がありません

markdownlint の MD040 警告対象です。コードブロックに言語(text など)を指定してください。

修正案
-```
+```text
 src/
 ├── __mocks__/              # テスト用モック(canvas, Three.js等)
 ...
-```
+```
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` at line 57, The code block in AGENTS.md that shows the project
tree is missing a language tag and triggers markdownlint MD040; update the
opening fence from ``` to ```text (i.e., change the code fence that precedes
"src/ ├── __mocks__/" to ```text) so the block is language-specified and linting
passes, leaving the closing fence as-is.
locales/ja/translation.json-448-450 (1)

448-450: ⚠️ Potential issue | 🟡 Minor

画像送信の説明文が実装挙動とズレています。

Line 449 の「無視される可能性があります」は、実装上は送信をブロックしてエラー表示する動作と一致していません。挙動に合わせた文言にしてください。

差分案
-  "EnableMultiModalDescription": "画像送信機能を有効にします。非対応モデルの場合、画像は無視される可能性があります。",
+  "EnableMultiModalDescription": "画像送信機能を有効にします。非対応モデルまたは無効な設定では、画像付きメッセージは送信できません。",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@locales/ja/translation.json` around lines 448 - 450,
EnableMultiModalDescription currently says images "may be ignored", but the
implementation blocks the send and shows an error; update the translation for
the key "EnableMultiModalDescription" to reflect that image sending will be
prevented and an error will be shown for unsupported models (and ensure the
wording aligns with the existing "MultiModalNotSupported" string and the
"EnableMultiModal" label).
src/components/settings/presenceSettings.tsx-117-121 (1)

117-121: ⚠️ Potential issue | 🟡 Minor

無効化条件の追加に対して説明文言が追随していません。

Line 117 で gameCommentaryEnabled を無効化条件に含めたため、PresenceDetectionDisabledInfo の文言もゲーム実況モードを理由として明記してください。現状だと「なぜ無効なのか」が画面表示と一致しません。

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

In `@src/components/settings/presenceSettings.tsx` around lines 117 - 121, Update
the displayed explanation in PresenceDetectionDisabledInfo to include game
commentary as a disabling reason: locate the PresenceDetectionDisabledInfo
component or the place where its message is composed and add "ゲーム実況モード" (or the
appropriate localized wording) to the list of reasons when gameCommentaryEnabled
is part of the disable condition, ensuring the text matches the boolean
condition that now includes gameCommentaryEnabled so the UI and logic remain
consistent.
.github/workflows/test.yml-130-133 (1)

130-133: ⚠️ Potential issue | 🟡 Minor

Run E2E tests ジョブに E2E_PORT を設定してください。

通常の E2E テストジョブ (行 76-77) には E2E_PORT が設定されていない一方で、本番 E2E テストジョブ (行 130-133) には E2E_PORT: '3201' が設定されています。ドキュメント (tests/e2e/README.md) では通常の E2E テストで E2E_PORT=3200 を使用するよう記載されているため、一貫性のために「Run E2E tests」ステップにも E2E_PORT: '3200' を追加してください。現在は Playwright の設定ファイルのデフォルト値 (3100) が使用されています。

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

In @.github/workflows/test.yml around lines 130 - 133, The "Run E2E tests"
GitHub Actions step is missing the E2E_PORT environment variable and is falling
back to the Playwright default (3100); add E2E_PORT: '3200' to the "Run E2E
tests" step so it matches the documented test port (tests/e2e/README.md) and the
production step uses 3201; locate the step with the name "Run E2E tests" in the
workflow and add an env block containing E2E_PORT: '3200'.
.agents/skills/sync-translations/SKILL.md-39-41 (1)

39-41: ⚠️ Potential issue | 🟡 Minor

フェンス付きコードブロックに言語指定を付けてください。

MD040 が出ているので、少なくとも textbash を付けておくと lint を消せます。

修正例
-```
+```text
 locales/ja/translation.json

@@

- +text
/sync-translations

Also applies to: 84-86

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

In @.agents/skills/sync-translations/SKILL.md around lines 39 - 41, The markdown
fenced code blocks in .agents/skills/sync-translations/SKILL.md (the blocks
showing "locales/ja/translation.json" and "/sync-translations") are missing a
language specifier which triggers MD040; update those backtick-fenced blocks to
include a language tag (e.g., use ```text or ```bash) so the linter passes, and
make the same change for the other affected block referenced around lines 84-86.
locales/pt/translation.json-656-656 (1)

656-656: ⚠️ Potential issue | 🟡 Minor

排他理由の説明文が実際の条件と一致していません

文言には Presence DetectionSlide Mode が含まれておらず、UI挙動と説明がズレます。実際に無効化トリガーになるモードを列挙してください。

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

In `@locales/pt/translation.json` at line 656, The translation string for the key
"DisabledByExclusion" is missing two modes (Presence Detection and Slide Mode)
and thus doesn't match the UI behavior; update the Portuguese value for
"DisabledByExclusion" to enumerate all modes that disable game comment mode
(Realtime API, Voice Mode, External Integration, Idle Mode, Presence Detection,
Slide Mode), preserving the existing punctuation/style and Portuguese phrasing
so the message clearly lists every disabling trigger.
tests/e2e/README.md-35-35 (1)

35-35: ⚠️ Potential issue | 🟡 Minor

テスト件数の固定値は早期に陳腐化します

13 tests / 12 tests / 1 test のような固定数値は運用中にすぐズレるため、READMEの信頼性を落とします。--list で確認する方針だけ残し、具体件数は削るのが安全です。

Also applies to: 68-68

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

In `@tests/e2e/README.md` at line 35, The README currently hardcodes test counts
("13 tests / 12 tests / 1 test"), which will become stale; update the README
text around the Playwright guidance (references to "npx playwright test --list",
"chromium project", "mobile-chromium project", and "*.mobile.spec.ts") to remove
the specific numeric counts and instead instruct readers to run `npx playwright
test --list` to see current totals and describe which project runs desktop vs
mobile specs (i.e., "chromium runs desktop specs" and "mobile-chromium runs
mobile-*.spec.ts") so the doc remains accurate without fixed numbers.
.agents/skills/update-docs/SKILL.md-150-157 (1)

150-157: ⚠️ Potential issue | 🟡 Minor

コードフェンスに言語指定がありません(MD040)

Markdown lint 警告どおり、2箇所でフェンス言語が未指定です。

差分案
-```
+```text
 英語翻訳エージェントと中国語翻訳エージェントを同時に起動する。
 各エージェントには以下を伝える:
 - 変更されたファイルの一覧
 - 各ファイルの変更内容の要約
 - 日本語ファイルとen/zh対応ファイルの両方を読んでから編集すること
 - 完了後にnpm run docs:buildでビルド検証すること

- +text
docs: v{バージョン}対応 - {主要変更の要約}(日英中)

{詳細な変更内容をリスト形式で}

Co-Authored-By: Codex Opus 4.6 noreply@anthropic.com

Also applies to: 198-204

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

In @.agents/skills/update-docs/SKILL.md around lines 150 - 157, Two fenced code
blocks in SKILL.md are missing language identifiers causing MD040; update both
blocks (the Japanese instruction block and the docs/version block that contains
"docs: v{バージョン}... Co-Authored-By...") to include a language tag (use "text") on
the opening fences (e.g. change ``` to ```text) and ensure the closing fences
remain, so the markdown linter no longer flags the missing fence language.
src/__tests__/features/gameCommentary/generateGameCommentary.test.ts-101-119 (1)

101-119: ⚠️ Potential issue | 🟡 Minor

console.error のモック復元漏れリスクがあります

途中で assertion が落ちると復元されず、後続テストに影響します。jest.spyOn(...).mockRestore() に寄せるのが安全です。

差分案
   it('returns null without logging an API error when commentary generation is aborted', async () => {
     const abortError = new DOMException('Aborted', 'AbortError')
-    const originalConsoleError = console.error
-    console.error = jest.fn()
+    const consoleErrorSpy = jest
+      .spyOn(console, 'error')
+      .mockImplementation(() => {})
     ;(getAIChatResponseStream as jest.Mock).mockRejectedValue(abortError)
 
-    await expect(
-      generateGameCommentary([], 'data:image/jpeg;base64,current', [], [], {
-        signal: new AbortController().signal,
-      })
-    ).resolves.toBeNull()
+    try {
+      await expect(
+        generateGameCommentary([], 'data:image/jpeg;base64,current', [], [], {
+          signal: new AbortController().signal,
+        })
+      ).resolves.toBeNull()
 
-    expect(console.error).not.toHaveBeenCalledWith(
-      'ゲーム実況コメント生成エラー:',
-      abortError
-    )
-
-    console.error = originalConsoleError
+      expect(console.error).not.toHaveBeenCalledWith(
+        'ゲーム実況コメント生成エラー:',
+        abortError
+      )
+    } finally {
+      consoleErrorSpy.mockRestore()
+    }
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/features/gameCommentary/generateGameCommentary.test.ts` around
lines 101 - 119, Replace the manual console.error save/restore with a jest.spyOn
so the mock is always cleaned up even if assertions fail: in the test that mocks
getAIChatResponseStream and asserts generateGameCommentary returns null on
AbortError, use jest.spyOn(console, 'error').mockImplementation(...) and ensure
you call mockRestore() (or restore in a finally/afterEach) to revert the spy;
keep references to the test's existing symbols generateGameCommentary and
getAIChatResponseStream so the mock targets the same behavior.
tests/e2e/kiosk-mode.spec.ts-100-101 (1)

100-101: ⚠️ Potential issue | 🟡 Minor

固定 300ms 待ちだと送信抑止の検証が甘くなります。

ここは「300ms 以内に飛ばなかった」だけを見ているので、送信処理が少し遅れただけでもテストが通ります。/api/ai/* を route で捕捉して失敗させるか、送信完了側の UI を待ったうえでリクエスト数を確認しないと偽陽性になります。

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

In `@tests/e2e/kiosk-mode.spec.ts` around lines 100 - 101, The test currently uses
a fixed 300ms sleep and then checks aiRequests.length, which allows slow sends
to pass; replace the brittle await page.waitForTimeout(300) with a deterministic
interception or UI-driven check: either register a route handler for "/api/ai/*"
via page.route to fail/spy on requests (so you can assert no requests were
made), or use page.waitForRequest / page.waitForResponse with a short timeout to
assert that no matching request occurs, or wait for the specific "send-complete"
UI state before asserting aiRequests.length is 0; remove the fixed timeout and
update assertions around aiRequests (and any test helpers that populate
aiRequests) to rely on these deterministic signals instead of sleep.

Comment thread tests/e2e/helpers/app.ts
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 27, 2026

Claude finished @tegnike's task —— View job


🔍 Comprehensive PR Review

I've completed a thorough analysis of this substantial PR. Here's my detailed review focusing on code quality, security, performance, and test coverage.

Review Checklist:

  • Read repository guidelines (CLAUDE.md)
  • Analyze major new features (Game Commentary Mode)
  • Review security implications
  • Check code quality and best practices
  • Evaluate test coverage
  • Assess performance considerations
  • Review locale changes
  • Check CI/CD updates
  • Provide actionable feedback

🎯 Major Features Assessment

Game Commentary Mode Implementation

Excellent overall implementation with proper architecture:

  • Strong separation of concerns: Clean type definitions in gameCommentaryTypes.ts, well-structured generation logic, singleton pattern for CaptureService
  • Robust validation: Input clamping functions with proper constants
  • Background analysis feature: Smart supplementary scene analysis during speech
  • Good error handling: Proper stream cleanup and AbortController support
  • Comprehensive testing: Full test coverage including edge cases and abort scenarios

Playwright E2E Testing Infrastructure

Professional testing setup:

  • Well-configured: Separate development/production test modes
  • Comprehensive coverage: Tests for all major features including game commentary
  • Good CI integration: Proper GitHub Actions workflow setup
  • Helper abstractions: Reusable test helpers in /tests/e2e/helpers/

Node.js 24.x + npm 11.6.2+ Toolchain Update

Appropriate modernization following repository standards


🚨 Critical Issues Found

1. Locale Translation Policy Violation 🔴

High Priority: The PR manually updates 15 non-Japanese locale files, violating the repository's translation policy:

Per CLAUDE.md: "言語ファイルの更新は日本語(/locales/ja/)のみ行う"

Files affected: All locale files except ja/ contain manually added keys like CopyEnvVars, CopyEnvVarsInfo, GameCommentary sections.

Impact: This conflicts with the automated translation workflow and creates maintenance burden.

Recommendation: Revert all non-Japanese locale changes and keep only the Japanese source updates.

2. Game Commentary NaN Validation Issue 🟠

Location: src/features/gameCommentary/gameCommentaryTypes.ts:65-95

Issue: Clamp functions don't handle NaN inputs properly:

export function clampCaptureInterval(value: number): number {
  if (value < GAME_COMMENTARY_INTERVAL.MIN) return GAME_COMMENTARY_INTERVAL.MIN
  if (value > GAME_COMMENTARY_INTERVAL.MAX) return GAME_COMMENTARY_INTERVAL.MAX
  return value // NaN passes through because NaN comparisons are false
}

Impact: Invalid form inputs can propagate NaN into timing calculations, breaking the game commentary scheduler.

Fix: Add Number.isFinite(value) check at the start of each clamp function.

3. Memory Leak in Video Delay Feature 🟠

Location: src/components/capture.tsx:127-229

Issues:

  • ImageBitmap objects not properly cleaned up when component unmounts during async operations
  • Uses .then() pattern instead of repository's async/await standard
  • Race condition between Promise resolution and cleanup

Impact: Accumulating ImageBitmaps can cause significant memory usage in game commentary mode.


⚠️ Medium Priority Issues

4. AbortController Handling Inconsistency

Location: src/features/chat/openAIAudioChat.ts:57-80

Issue: AbortSignal check uses break instead of throwing AbortError:

if (options.signal?.aborted) break  // Wrong: continues to flush/close

Impact: Aborted requests are treated as successful completions.

Fix: Throw DOMException with name 'AbortError' instead of break.

5. Missing AbortSignal Propagation in Memory Search

Location: src/features/memory/memoryStoreSync.ts:79-92

Issue: Promise.race() timeout doesn't cancel underlying operations.

Impact: Slow backends accumulate uncancelled memory searches, wasting API calls.

Recommendation: Add AbortSignal support to searchMemories() and getEmbedding().

6. SpeakQueue Session Cleanup Issue

Location: src/features/messages/speakQueue.ts:84-96

Issue: stopSession() doesn't call onComplete() for removed tasks.

Impact: Waiting callers may never resolve.


🔒 Security Assessment: ✅ SECURE

  • No hardcoded API keys or secrets
  • Proper environment variable handling
  • Test data uses safe mock values
  • No unsafe DOM manipulation in capture service
  • Input validation present (though NaN handling needs improvement)

🚀 Performance Analysis

Strengths:

  • Efficient resource management: Singleton patterns prevent memory leaks
  • Proper cleanup patterns: Good use of cleanup functions
  • Reasonable capture intervals: Configurable quality/size settings

Concerns:

  • Continuous 60fps redraw: VideoDisplay.tsx:121-162 redraws canvas every frame even when content unchanged
  • Image processing blocking: captureService.ts uses synchronous image loading

🧪 Test Coverage: ⭐⭐⭐⭐⭐ Excellent

Strong Coverage:

  • ✅ Game commentary logic: comprehensive unit tests
  • ✅ E2E workflows: tests for all major user flows
  • ✅ Store exclusion rules: thorough state management testing
  • ✅ Message processing: proper concurrency testing
  • ✅ AbortController scenarios: including timeout handling

Minor Gaps:

  • Image processing error paths in captureService.ts
  • Some edge cases in background analysis intervals

📋 Code Quality Assessment

Excellent Patterns:

  • Architecture: Follows existing patterns (singleton services, factory pattern)
  • TypeScript: Comprehensive type definitions with proper error handling
  • Error boundaries: Good error catching and logging
  • Separation of concerns: Clean module boundaries

Areas for Improvement:

  • Mixed async patterns: Some .then() usage alongside async/await
  • Race conditions: Video delay feature needs disposal flag
  • Input validation: NaN handling in validation functions

🏗️ CI/CD Updates Assessment: ✅ Good

  • Node.js 24.x pinning: Appropriate and consistent
  • E2E test integration: Well-structured with proper artifacts
  • Missing E2E_PORT: Development E2E tests should specify port for consistency

📝 Specific Recommendations

High Priority:

  1. Revert non-Japanese locale files to comply with translation policy
  2. Fix NaN validation in game commentary clamp functions
  3. Implement proper cleanup for video delay ImageBitmaps

Medium Priority:

  1. Standardize async patterns - replace .then() with async/await
  2. Add AbortSignal propagation to memory search operations
  3. Fix AbortController handling in audio chat

Low Priority:

  1. Optimize video rendering to skip unnecessary redraws
  2. Add missing test ID to avoid E2E test duplication

🎉 Overall Assessment

This is a high-quality PR that significantly enhances the application while maintaining architectural integrity. The game commentary feature is well-implemented with proper testing, and the E2E testing infrastructure will benefit future development.

Code Quality: ⭐⭐⭐⭐⭐
Feature Implementation: ⭐⭐⭐⭐⭐
Test Coverage: ⭐⭐⭐⭐⭐
Security: ⭐⭐⭐⭐⭐
Performance: ⭐⭐⭐⭐⭐

Recommendation: Approve with minor fixes - The critical issues are easily addressable and don't affect the core functionality.


Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (1)
locales/ko/translation.json (1)

96-691: ⚠️ Potential issue | 🟠 Major

非日本語ロケールの手動追加は運用方針と不整合です。

このPRでは locales/ko/translation.json に新規キーを直接追加しています。運用が「新規翻訳キー追加時は /locales/ja/ のみ更新」の前提なら、翻訳ソースの一貫性が崩れるため、追加元を /locales/ja/ に寄せて既存の同期フローで反映する形にしてください。

Based on learnings: Update only Japanese language files (/locales/ja/) when adding new translation strings; do not manually update other language files

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

In `@locales/ko/translation.json` around lines 96 - 691, You added many new
translation keys (e.g., "CopyEnvVars", "CopyEnvVarsInfo",
"ChangeBackgroundImage", etc.) directly into the Korean locale file, which
breaks the project's translation workflow; remove these new keys from the Korean
file and instead add them to the Japanese source locale (the ja translation
file) so they enter the canonical sync pipeline, then run the project's
localization sync/export process to propagate translations to other locales
rather than manually editing the ko translation file.
🧹 Nitpick comments (4)
.github/workflows/test.yml (2)

25-33: toolchain セットアップの重複を composite action または reusable workflow で共通化することを検討してください。

Setup npmVerify toolchain ステップが teste2ee2e-production の 3 つの job で同一のコードとして重複しています。特に Verify toolchain のインライン Node.js スクリプトは複雑で、変更時に 3 箇所を同期する必要があります。

将来的に composite action または reusable workflow に抽出することで、メンテナンス性が向上します。

📝 Composite action の例

.github/actions/setup-toolchain/action.yml:

name: 'Setup Toolchain'
description: 'Setup Node.js 24.x and npm 11.6.2'
runs:
  using: 'composite'
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '24.x'
        cache: 'npm'
    - name: Setup npm
      shell: bash
      run: npm install -g npm@^11.6.2
    - name: Verify toolchain
      shell: bash
      run: |
        node --version
        npm --version
        node -e "const { execSync } = require('child_process'); const nodeMajor = Number(process.versions.node.split('.')[0]); const npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim().split('.').map(Number); if (nodeMajor !== 24 || npmVersion[0] !== 11 || npmVersion[1] < 6 || (npmVersion[1] === 6 && npmVersion[2] < 2)) process.exit(1);"

各 job での使用:

- uses: ./.github/actions/setup-toolchain

Also applies to: 58-66, 114-122

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

In @.github/workflows/test.yml around lines 25 - 33, Extract the duplicated
"Setup npm" and "Verify toolchain" steps (the inline Node.js verification logic)
used in the test, e2e, and e2e-production jobs into a single reusable unit
(either a composite action or a reusable workflow) and replace each in
.github/workflows/test.yml with a single call; specifically move the npm install
-g npm@^11.6.2 step and the node -e verification script into the new composite
action/reusable workflow, expose any inputs if needed, and update the three jobs
to use the new unit so the complex verification logic in the "Verify toolchain"
step is maintained in one place.

43-98: E2E job に timeout-minutes の設定を推奨します。

E2E テストは実行時間が長くなりがちで、何らかの問題でハングした場合に CI リソースを長時間占有する可能性があります。明示的なタイムアウトを設定することで、予期しない長時間実行を防げます。

📝 修正案
   e2e:
     runs-on: ubuntu-latest
+    timeout-minutes: 30

     steps:
   e2e-production:
     runs-on: ubuntu-latest
+    timeout-minutes: 30

     steps:

Also applies to: 99-153

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

In @.github/workflows/test.yml around lines 43 - 98, Add an explicit job timeout
for the e2e workflow: in the job named "e2e" (the block starting with "e2e:"
that contains steps like "Install dependencies", "Run E2E tests", etc.) add the
GitHub Actions key timeout-minutes: <minutes> (e.g., 60) at the same indentation
as "runs-on" to limit how long the job can run; also apply the same
timeout-minutes setting to the other E2E job(s) referenced elsewhere in this
workflow (the other block that runs Playwright tests/listing) so both places use
a bounded timeout.
tests/e2e/README.md (1)

132-144: CI 運用セクションの実行順序が実際の workflow と異なります。

このセクションでは npm run test:e2e から npm run test:e2e:production までを一連の流れとして記載していますが、実際の .github/workflows/test.yml では e2e job と e2e-production job は別の job として並列実行されます。

ドキュメントの読者が CI の実際の動作を誤解しないよう、job が分離されていることを明記することを検討してください。

📝 修正案
 CI では次の順で実行します。
+
+**`e2e` job:**

 ```bash
 npm install -g npm@^11.6.2
 node --version
 npm --version
 npm ci
 npm run test:e2e:install:with-deps
 npm run test:e2e -- --list
 npm run test:e2e
+```
+
+**`e2e-production` job (並列実行):**
+
+```bash
+npm install -g npm@^11.6.2
+node --version
+npm --version
+npm ci
+npm run test:e2e:install:with-deps
 npm run test:e2e:production -- --list
 E2E_PORT=3201 npm run test:e2e:production
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @tests/e2e/README.md around lines 132 - 144, Update the CI section to reflect
that the "e2e" and "e2e-production" jobs run as separate, parallel jobs in the
workflow: split the single command sequence into two labeled subsections ("e2e"
and "e2e-production (parallel)") and move the production-specific commands (npm
run test:e2e:production -- --list and E2E_PORT=3201 npm run test:e2e:production)
into the production subsection; reference the job names e2e and e2e-production
and the workflow .github/workflows/test.yml so readers understand this mirrors
the actual workflow job separation.


</details>

</blockquote></details>
<details>
<summary>tests/e2e/kiosk-mode.spec.ts (1)</summary><blockquote>

`22-25`: **`evaluate(...click())` ではなく `locator.click()` を使ってください**

Line 23-25 は DOM の `click()` を直接叩くため、Playwright の待機・操作性チェックを迂回し、E2E の偽陽性につながりやすいです。

<details>
<summary>修正案</summary>

```diff
 async function clickElement(page: Page, testId: string) {
-  await page.getByTestId(testId).evaluate((element) => {
-    ;(element as HTMLElement).click()
-  })
+  await page.getByTestId(testId).click({ force: true })
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/kiosk-mode.spec.ts` around lines 22 - 25, The helper clickElement
uses page.getByTestId(...).evaluate(...) to call DOM click(), which bypasses
Playwright's built-in waits and can cause flaky E2E results; change it to use
Playwright's locator click API instead—replace the evaluate call with
page.getByTestId(testId).click() (or assign to a locator and call
locator.click()) inside the clickElement function so Playwright's waiting and
actionability checks run correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@locales/de/translation.json`:
- Around line 96-97: Don't edit non-Japanese locale files directly; revert the
changes to the German file (the keys "CopyEnvVars" and "CopyEnvVarsInfo") and
instead add the new translation keys and Japanese source strings to the Japanese
locale under the same key names in /locales/ja/ (e.g., add "CopyEnvVars" and
"CopyEnvVarsInfo" entries to the ja file), then run the repository's
localization sync process to propagate translations to other locales; apply the
same fix for the other affected ranges (around the keys referenced for lines
308, 435-436, 524, 533, and 631-691) so only ja is manually updated and non-ja
files are kept in sync automatically.

In `@locales/en/translation.json`:
- Around line 96-97: Revert the manual edits to locales/en/translation.json for
the keys "CopyEnvVars" and "CopyEnvVarsInfo" (and any other changes in the
ranges referenced: 308, 435-436, 524, 533, 631-691), then add the new
translation keys only to locales/ja/translation.json (or submit them to the
sync-translations flow) so the canonical Japanese file contains the new entries
and the automated sync will propagate them to other locales; locate the keys by
name ("CopyEnvVars", "CopyEnvVarsInfo") and ensure no direct edits remain in
non-ja locale files.

In `@locales/hi/translation.json`:
- Around line 96-97: The new localization keys "CopyEnvVars" and
"CopyEnvVarsInfo" were added directly into the Hindi locale which breaks the
master-Japanese sync flow; remove these keys from locales/hi/translation.json
(and any other non-ja files listed: the ranges noted) and instead add/update
them only in the Japanese master file (locales/ja/translation.json), then run
the project's locale sync/update script or workflow so translations propagate
automatically to other languages; ensure you reference the exact keys
"CopyEnvVars" and "CopyEnvVarsInfo" when updating the ja file so the sync picks
them up.

In `@locales/zh-TW/translation.json`:
- Around line 96-97: The zh-TW locale file contains manual additions for keys
"CopyEnvVars" and "CopyEnvVarsInfo" (and other ranges noted) which violates the
rule to only update Japanese source files; revert these manual edits in
locales/zh-TW/translation.json and instead add/update the corresponding keys in
the source Japanese locale under /locales/ja/ (using the exact key names
"CopyEnvVars" and "CopyEnvVarsInfo"), then run the project's i18n
synchronization tool/process to propagate translations to other language files
so the zh-TW file is generated automatically rather than edited by hand.

In `@src/hooks/useGameCommentaryMode.ts`:
- Around line 350-361: The await call to captureService.captureFrame is outside
any try/catch so if it rejects the loop stays stuck with isProcessingRef.current
true and state left as 'capturing'; wrap the captureFrame call (both at the
shown block and the similar block around lines 472-483) in a try/catch, and in
the catch ensure you reset isProcessingRef.current = false, call
setState('waiting') and scheduleNext() before returning (or handling the error),
so failures during capture always clean up and reschedule the loop; keep
references to captureService.captureFrame, isProcessingRef.current, setState,
and scheduleNext when making the change.
- Around line 345-389: After captureFrame returns, validate that the request is
still current and running before calling generateGameCommentary: compare
commentaryRequestTokenRef.current to the local requestToken and ensure
isRunningRef.current is true and stateRef.current is still 'capturing' (or not
'stopped'); if any check fails, set isProcessingRef.current = false, clear
commentaryAbortControllerRef.current, call scheduleNext() (or
setState('waiting')) and return early to avoid calling generateGameCommentary
and wasting API cost; apply this check immediately after imageData is obtained
and before creating the AbortController / calling generateGameCommentary so
stopCommentary() advancement of commentaryRequestTokenRef prevents stale
processing.
- Around line 548-551: アンマウント時の cleanup がタイマーのみを止めており進行中の実況生成や SpeakQueue
セッションが残るため、現在の戻り値関数(現在は clearTimers() と clearBackgroundAnalysisTimer() を呼ぶ箇所)に
invalidateActiveCommentary() を呼んで実況の状態を無効化し、さらに
SpeakQueue.stopSession(sessionIdRef.current)
を呼び出して現在のセッションを明示的に停止する処理を追加してください。これにより後続の完了コールバック経由での
setState()/scheduleNext() 実行経路を遮断できます(参照シンボル: invalidateActiveCommentary,
SpeakQueue.stopSession, sessionIdRef.current, clearTimers,
clearBackgroundAnalysisTimer)。

In `@tests/e2e/kiosk-mode.spec.ts`:
- Around line 89-102: The test currently checks aiRequests immediately with
expect(aiRequests).toHaveLength(0), which can miss delayed network calls;
replace this synchronous check with an explicit wait-for-no-request using
Playwright's waitForRequest on the page (targeting paths starting with
'/api/ai/') with a short timeout and assert that it times out (or catch the
timeout and treat it as success), or alternatively use page.waitForRequest in a
try/catch to fail if any matching request arrives within the timeout; update the
block around page.on('request', ...) and the final assertion to use
page.waitForRequest (or equivalent timeout-based logic) to deterministically
confirm no AI request was sent.

---

Outside diff comments:
In `@locales/ko/translation.json`:
- Around line 96-691: You added many new translation keys (e.g., "CopyEnvVars",
"CopyEnvVarsInfo", "ChangeBackgroundImage", etc.) directly into the Korean
locale file, which breaks the project's translation workflow; remove these new
keys from the Korean file and instead add them to the Japanese source locale
(the ja translation file) so they enter the canonical sync pipeline, then run
the project's localization sync/export process to propagate translations to
other locales rather than manually editing the ko translation file.

---

Nitpick comments:
In @.github/workflows/test.yml:
- Around line 25-33: Extract the duplicated "Setup npm" and "Verify toolchain"
steps (the inline Node.js verification logic) used in the test, e2e, and
e2e-production jobs into a single reusable unit (either a composite action or a
reusable workflow) and replace each in .github/workflows/test.yml with a single
call; specifically move the npm install -g npm@^11.6.2 step and the node -e
verification script into the new composite action/reusable workflow, expose any
inputs if needed, and update the three jobs to use the new unit so the complex
verification logic in the "Verify toolchain" step is maintained in one place.
- Around line 43-98: Add an explicit job timeout for the e2e workflow: in the
job named "e2e" (the block starting with "e2e:" that contains steps like
"Install dependencies", "Run E2E tests", etc.) add the GitHub Actions key
timeout-minutes: <minutes> (e.g., 60) at the same indentation as "runs-on" to
limit how long the job can run; also apply the same timeout-minutes setting to
the other E2E job(s) referenced elsewhere in this workflow (the other block that
runs Playwright tests/listing) so both places use a bounded timeout.

In `@tests/e2e/kiosk-mode.spec.ts`:
- Around line 22-25: The helper clickElement uses
page.getByTestId(...).evaluate(...) to call DOM click(), which bypasses
Playwright's built-in waits and can cause flaky E2E results; change it to use
Playwright's locator click API instead—replace the evaluate call with
page.getByTestId(testId).click() (or assign to a locator and call
locator.click()) inside the clickElement function so Playwright's waiting and
actionability checks run correctly.

In `@tests/e2e/README.md`:
- Around line 132-144: Update the CI section to reflect that the "e2e" and
"e2e-production" jobs run as separate, parallel jobs in the workflow: split the
single command sequence into two labeled subsections ("e2e" and "e2e-production
(parallel)") and move the production-specific commands (npm run
test:e2e:production -- --list and E2E_PORT=3201 npm run test:e2e:production)
into the production subsection; reference the job names e2e and e2e-production
and the workflow .github/workflows/test.yml so readers understand this mirrors
the actual workflow job separation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d2bc4609-7258-4050-9871-d041961df6aa

📥 Commits

Reviewing files that changed from the base of the PR and between ca52c17 and 0b2bc6d.

📒 Files selected for processing (25)
  • .agents/skills/sync-translations/SKILL.md
  • .agents/skills/update-docs/SKILL.md
  • .github/workflows/test.yml
  • locales/ar/translation.json
  • locales/de/translation.json
  • locales/en/translation.json
  • locales/es/translation.json
  • locales/fr/translation.json
  • locales/hi/translation.json
  • locales/it/translation.json
  • locales/ja/translation.json
  • locales/ko/translation.json
  • locales/pl/translation.json
  • locales/pt/translation.json
  • locales/ru/translation.json
  • locales/th/translation.json
  • locales/vi/translation.json
  • locales/zh-CN/translation.json
  • locales/zh-TW/translation.json
  • src/__tests__/features/gameCommentary/generateGameCommentary.test.ts
  • src/features/gameCommentary/captureService.ts
  • src/hooks/useGameCommentaryMode.ts
  • tests/e2e/README.md
  • tests/e2e/helpers/app.ts
  • tests/e2e/kiosk-mode.spec.ts
✅ Files skipped from review due to trivial changes (1)
  • .agents/skills/update-docs/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (11)
  • locales/fr/translation.json
  • locales/zh-CN/translation.json
  • tests/e2e/helpers/app.ts
  • locales/it/translation.json
  • locales/th/translation.json
  • locales/vi/translation.json
  • src/features/gameCommentary/captureService.ts
  • locales/pt/translation.json
  • locales/ar/translation.json
  • locales/ru/translation.json
  • locales/es/translation.json

Comment on lines +96 to +97
"CopyEnvVars": "Umgebungsvariablen kopieren",
"CopyEnvVarsInfo": "Kopiert die aktuelle Charakterposition im Format von Umgebungsvariablen in die Zwischenablage. Sie können sie in Ihre .env-Datei einfügen.",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

他言語翻訳の直接編集は避けたいです。

このリポジトリの運用だと、新規翻訳キーは日本語ファイルを起点に同期する前提です。de をここで直接更新すると、以後の同期と差分管理が壊れやすいので、追加分は ja 側だけを手動更新して同期処理に任せるほうが安全です。

As per coding guidelines "Update language files only in Japanese (/locales/ja/); do not manually update other language files".

Also applies to: 308-308, 435-436, 524-524, 533-533, 631-691

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

In `@locales/de/translation.json` around lines 96 - 97, Don't edit non-Japanese
locale files directly; revert the changes to the German file (the keys
"CopyEnvVars" and "CopyEnvVarsInfo") and instead add the new translation keys
and Japanese source strings to the Japanese locale under the same key names in
/locales/ja/ (e.g., add "CopyEnvVars" and "CopyEnvVarsInfo" entries to the ja
file), then run the repository's localization sync process to propagate
translations to other locales; apply the same fix for the other affected ranges
(around the keys referenced for lines 308, 435-436, 524, 533, and 631-691) so
only ja is manually updated and non-ja files are kept in sync automatically.

Comment on lines +96 to +97
"CopyEnvVars": "Copy environment variables",
"CopyEnvVarsInfo": "Copy the current character position to the clipboard in environment variable format. You can paste it into your .env file.",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

他言語ロケールは手動更新しないでください。

このリポジトリでは新しい翻訳キーは locales/ja/translation.json を更新し、他言語は同期フローで反映する前提です。このファイルを直接編集すると、次回同期時に差分競合や訳語ずれの温床になります。今回の追加分は sync-translations 側へ寄せる形にしたいです。

As per coding guidelines "Update language files only in Japanese (/locales/ja/); do not manually update other language files".

Also applies to: 308-308, 435-436, 524-524, 533-533, 631-691

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

In `@locales/en/translation.json` around lines 96 - 97, Revert the manual edits to
locales/en/translation.json for the keys "CopyEnvVars" and "CopyEnvVarsInfo"
(and any other changes in the ranges referenced: 308, 435-436, 524, 533,
631-691), then add the new translation keys only to locales/ja/translation.json
(or submit them to the sync-translations flow) so the canonical Japanese file
contains the new entries and the automated sync will propagate them to other
locales; locate the keys by name ("CopyEnvVars", "CopyEnvVarsInfo") and ensure
no direct edits remain in non-ja locale files.

Comment on lines +96 to +97
"CopyEnvVars": "पर्यावरण चर कॉपी करें",
"CopyEnvVarsInfo": "वर्तमान कैरेक्टर स्थिति को पर्यावरण चर प्रारूप में क्लिपबोर्ड पर कॉपी करता है। आप इसे .env फ़ाइल में पेस्ट कर सकते हैं।",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

このロケール追加は手動ではなく同期フローに寄せてください。

新規キーを locales/hi/translation.json に直接追加すると、ja をマスターにした同期運用とずれてメンテが難しくなります。ここは locales/ja/translation.json のみを手動更新し、他言語は同期スキルで反映する形に揃えたいです。

As per coding guidelines "Update language files only in Japanese (/locales/ja/); do not manually update other language files".

Also applies to: 308-308, 435-436, 524-524, 533-533, 631-691

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

In `@locales/hi/translation.json` around lines 96 - 97, The new localization keys
"CopyEnvVars" and "CopyEnvVarsInfo" were added directly into the Hindi locale
which breaks the master-Japanese sync flow; remove these keys from
locales/hi/translation.json (and any other non-ja files listed: the ranges
noted) and instead add/update them only in the Japanese master file
(locales/ja/translation.json), then run the project's locale sync/update script
or workflow so translations propagate automatically to other languages; ensure
you reference the exact keys "CopyEnvVars" and "CopyEnvVarsInfo" when updating
the ja file so the sync picks them up.

Comment on lines +96 to +97
"CopyEnvVars": "複製環境變數",
"CopyEnvVarsInfo": "將目前角色位置以環境變數格式複製到剪貼簿。可貼到 .env 檔案中使用。",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

このファイルも同期生成に寄せてください。

zh-TW への直接追加は、ja をソース・オブ・トゥルースにする翻訳運用と噛み合っていません。手動更新は日本語だけに留めて、他言語は同期ツール経由に統一したいです。

As per coding guidelines "Update language files only in Japanese (/locales/ja/); do not manually update other language files".

Also applies to: 308-308, 435-436, 524-524, 533-533, 631-691

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

In `@locales/zh-TW/translation.json` around lines 96 - 97, The zh-TW locale file
contains manual additions for keys "CopyEnvVars" and "CopyEnvVarsInfo" (and
other ranges noted) which violates the rule to only update Japanese source
files; revert these manual edits in locales/zh-TW/translation.json and instead
add/update the corresponding keys in the source Japanese locale under
/locales/ja/ (using the exact key names "CopyEnvVars" and "CopyEnvVarsInfo"),
then run the project's i18n synchronization tool/process to propagate
translations to other language files so the zh-TW file is generated
automatically rather than edited by hand.

Comment on lines +345 to +389
isProcessingRef.current = true
setState('capturing')
const requestToken = commentaryRequestTokenRef.current + 1
commentaryRequestTokenRef.current = requestToken

// キャプチャ取得
const imageData = await captureService.captureFrame(
gameCommentaryResizeWidth,
gameCommentaryImageQuality
)

if (!imageData) {
console.warn('ゲーム実況: キャプチャ取得失敗')
isProcessingRef.current = false
setState('waiting')
scheduleNext()
return
}

// AI実況コメント生成
const abortController = new AbortController()
commentaryAbortControllerRef.current = abortController

try {
const backgroundSceneAnalyses = backgroundSceneAnalysesRef.current
resetBackgroundSceneAnalyses()

// chatLogから直近メッセージを取得(視聴者コメントとの文脈共有)
const maxPastMessages = settingsStore.getState().maxPastMessages
const chatLog = homeStore.getState().chatLog
const recentMessages = chatLog
.filter((m) => m.role === 'user' || m.role === 'assistant')
.slice(maxPastMessages > 0 ? -maxPastMessages : 0)
.map((m) => ({
role: m.role,
content: typeof m.content === 'string' ? m.content : '',
}))

const result = await generateGameCommentary(
commentaryHistoryRef.current,
imageData,
recentMessages,
backgroundSceneAnalyses,
{ signal: abortController.signal }
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

停止後の古いキャプチャ結果で AI 呼び出しが続行されます。

stopCommentary() はトークンを進めますが、captureFrame() 完了直後にそのトークンを再確認していないため、キャプチャ中に停止しても generateGameCommentary() までは進んでしまいます。発話は抑止できても API コストは発生するので、imageData 取得直後に requestToken / isRunningRef / stateRef を再チェックして早期 return したいです。

💡 修正イメージ
     const imageData = await captureService.captureFrame(
       gameCommentaryResizeWidth,
       gameCommentaryImageQuality
     )
+
+    if (
+      requestToken !== commentaryRequestTokenRef.current ||
+      !isRunningRef.current ||
+      stateRef.current !== 'capturing'
+    ) {
+      isProcessingRef.current = false
+      return
+    }
 
     if (!imageData) {
       console.warn('ゲーム実況: キャプチャ取得失敗')
       isProcessingRef.current = false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useGameCommentaryMode.ts` around lines 345 - 389, After
captureFrame returns, validate that the request is still current and running
before calling generateGameCommentary: compare commentaryRequestTokenRef.current
to the local requestToken and ensure isRunningRef.current is true and
stateRef.current is still 'capturing' (or not 'stopped'); if any check fails,
set isProcessingRef.current = false, clear commentaryAbortControllerRef.current,
call scheduleNext() (or setState('waiting')) and return early to avoid calling
generateGameCommentary and wasting API cost; apply this check immediately after
imageData is obtained and before creating the AbortController / calling
generateGameCommentary so stopCommentary() advancement of
commentaryRequestTokenRef prevents stale processing.

Comment on lines +350 to +361
// キャプチャ取得
const imageData = await captureService.captureFrame(
gameCommentaryResizeWidth,
gameCommentaryImageQuality
)

if (!imageData) {
console.warn('ゲーム実況: キャプチャ取得失敗')
isProcessingRef.current = false
setState('waiting')
scheduleNext()
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

captureFrame() の例外で実況ループが固まります。

captureFrame()try の外で await されているので、ここが reject すると isProcessingRef.currenttrue のまま残り、state も capturing から戻りません。以後の再スケジュールも止まるので、キャプチャ取得も含めて例外処理の内側に入れる必要があります。

Also applies to: 472-483

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

In `@src/hooks/useGameCommentaryMode.ts` around lines 350 - 361, The await call to
captureService.captureFrame is outside any try/catch so if it rejects the loop
stays stuck with isProcessingRef.current true and state left as 'capturing';
wrap the captureFrame call (both at the shown block and the similar block around
lines 472-483) in a try/catch, and in the catch ensure you reset
isProcessingRef.current = false, call setState('waiting') and scheduleNext()
before returning (or handling the error), so failures during capture always
clean up and reschedule the loop; keep references to
captureService.captureFrame, isProcessingRef.current, setState, and scheduleNext
when making the change.

Comment on lines +548 to +551
return () => {
clearTimers()
clearBackgroundAnalysisTimer()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

アンマウント cleanup で進行中の実況を止めきれていません。

cleanup ではタイマーしか止めていないので、アンマウント時に進行中の生成や SpeakQueue セッションが残ります。完了コールバックから後で setState / scheduleNext() が走る経路も残るため、ここでも invalidateActiveCommentary()SpeakQueue.stopSession(sessionIdRef.current) まで実行したほうが安全です。

💡 修正イメージ
     return () => {
       clearTimers()
       clearBackgroundAnalysisTimer()
+      invalidateActiveCommentary()
+      SpeakQueue.stopSession(sessionIdRef.current)
+      sessionIdRef.current = null
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useGameCommentaryMode.ts` around lines 548 - 551, アンマウント時の cleanup
がタイマーのみを止めており進行中の実況生成や SpeakQueue セッションが残るため、現在の戻り値関数(現在は clearTimers() と
clearBackgroundAnalysisTimer() を呼ぶ箇所)に invalidateActiveCommentary()
を呼んで実況の状態を無効化し、さらに SpeakQueue.stopSession(sessionIdRef.current)
を呼び出して現在のセッションを明示的に停止する処理を追加してください。これにより後続の完了コールバック経由での
setState()/scheduleNext() 実行経路を遮断できます(参照シンボル: invalidateActiveCommentary,
SpeakQueue.stopSession, sessionIdRef.current, clearTimers,
clearBackgroundAnalysisTimer)。

Comment on lines +89 to +102
const aiRequests: string[] = []
page.on('request', (request) => {
const url = new URL(request.url())
if (url.pathname.startsWith('/api/ai/')) {
aiRequests.push(url.pathname)
}
})

await messageInput.fill('this contains blocked')
await page.getByTestId('chat-send-button').click({ force: true })

await expect(page.getByText('不適切な内容が含まれています')).toBeVisible()
expect(aiRequests).toHaveLength(0)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the test file exists
fd 'kiosk-mode.spec.ts' --type f

Repository: tegnike/aituber-kit

Length of output: 92


🏁 Script executed:

# Read the test file around lines 89-102 to verify the code snippet
head -110 tests/e2e/kiosk-mode.spec.ts | tail -n +85

Repository: tegnike/aituber-kit

Length of output: 810


🏁 Script executed:

# Get more context around this test to understand what it's testing
sed -n '75,115p' tests/e2e/kiosk-mode.spec.ts

Repository: tegnike/aituber-kit

Length of output: 1379


🏁 Script executed:

# Check for similar request listening patterns in other e2e tests
rg 'page\.on\(.*request' tests/e2e/ -A 3

Repository: tegnike/aituber-kit

Length of output: 529


🏁 Script executed:

# Check if waitForRequest is used elsewhere in the codebase
rg 'waitForRequest' tests/ --type ts --type tsx

Repository: tegnike/aituber-kit

Length of output: 91


🏁 Script executed:

# Get more context on the test to understand the content filtering mechanism
sed -n '60,110p' tests/e2e/kiosk-mode.spec.ts

Repository: tegnike/aituber-kit

Length of output: 1886


🏁 Script executed:

# Check Playwright documentation and common patterns for negative request assertions
# Search for other test patterns that might check for absence of requests
rg 'expect.*request' tests/e2e/ -A 2 -B 2

Repository: tegnike/aituber-kit

Length of output: 45


🏁 Script executed:

# Verify the content filtering logic - check what triggers the error message
rg '不適切な内容が含まれています' src/ --type ts --type js -B 5 -A 2

Repository: tegnike/aituber-kit

Length of output: 1093


🏁 Script executed:

# Check if the validation prevents the request from being made in the first place
rg -A 20 'validateInput' src/hooks/useKioskMode.ts | head -40

Repository: tegnike/aituber-kit

Length of output: 1387


🏁 Script executed:

# Look for where the AI request is actually made to understand the flow
rg 'api/ai/' src/ --type ts --type js -B 5 -A 5

Repository: tegnike/aituber-kit

Length of output: 3619


🏁 Script executed:

# Check how the validation is used in the chat sending flow
rg -B 10 -A 10 'validateInput' src/ --type ts | grep -A 20 'send\|submit\|request' | head -50

Repository: tegnike/aituber-kit

Length of output: 1401


🏁 Script executed:

# Look for the message send handler to understand the flow
rg 'chat-send-button\|handleSend\|onSend' src/ --type ts -B 3 -A 10 | head -60

Repository: tegnike/aituber-kit

Length of output: 45


🏁 Script executed:

# Check the actual validation logic more completely
sed -n '1,100p' src/hooks/useKioskMode.ts

Repository: tegnike/aituber-kit

Length of output: 2837


🏁 Script executed:

# Check if there's any async behavior in the message send handler
rg -A 30 'handleValidatedSend' src/components/messageInput.tsx

Repository: tegnike/aituber-kit

Length of output: 3229


🏁 Script executed:

# Look for the actual send logic after validation passes
rg -B 5 -A 20 'const handleSend\|function handleSend' src/components/messageInput.tsx

Repository: tegnike/aituber-kit

Length of output: 45


🏁 Script executed:

# Verify if waitForRequest is a valid Playwright API
rg 'waitForRequest' --type ts --type js -g '*.ts' -g '*.js' /

Repository: tegnike/aituber-kit

Length of output: 1512


🏁 Script executed:

# Check Playwright documentation by searching package.json for version
cat package.json | grep -i playwright -A 2 -B 2

Repository: tegnike/aituber-kit

Length of output: 881


🏁 Script executed:

# Search for actual request interception patterns used in the codebase
rg 'page\.on.*request\|waitFor.*request\|page\.route' tests/e2e/ --type ts -A 3

Repository: tegnike/aituber-kit

Length of output: 45


🏁 Script executed:

# Check Playwright API documentation by looking at type definitions
find node_modules/@playwright -name "*.d.ts" -type f | head -5

Repository: tegnike/aituber-kit

Length of output: 123


🏁 Script executed:

# Search for page.waitForRequest usage in Playwright types
rg 'waitForRequest' node_modules/@playwright/test/ 2>/dev/null | head -20

Repository: tegnike/aituber-kit

Length of output: 45


🏁 Script executed:

# Check if there are other patterns for testing absence of requests in e2e tests
rg 'page\.on.*request' tests/e2e/ --type ts -B 5 -A 15 | head -80

Repository: tegnike/aituber-kit

Length of output: 2304


🏁 Script executed:

# Check the Prettier configuration to understand the code style requirements
cat .prettierrc* prettier.config.* 2>/dev/null || cat package.json | grep -A 20 'prettier'

Repository: tegnike/aituber-kit

Length of output: 2490


🏁 Script executed:

# Check if the current code uses single quotes or double quotes
sed -n '89,102p' tests/e2e/kiosk-mode.spec.ts | cat -A

Repository: tegnike/aituber-kit

Length of output: 635


🏁 Script executed:

# Look at surrounding code in the test file to understand the pattern
sed -n '1,30p' tests/e2e/kiosk-mode.spec.ts

Repository: tegnike/aituber-kit

Length of output: 773


AI リクエスト検証のタイミング安定性を改善してください

Line 101 の即時 expect(aiRequests).toHaveLength(0) では、ネットワーク層での遅延リクエストを確実に拾えません。検証ロジック自体は同期的にリクエスト発火を防いでいますが、テスト安定性のため waitForRequest タイムアウトを使用して「一定時間リクエストがないこと」を明示的に確認する方が堅牢です。

修正案
-  const aiRequests: string[] = []
-  page.on('request', (request) => {
-    const url = new URL(request.url())
-    if (url.pathname.startsWith('/api/ai/')) {
-      aiRequests.push(url.pathname)
-    }
-  })
+  const aiRequestDetected = page
+    .waitForRequest((request) => {
+      const url = new URL(request.url())
+      return url.pathname.startsWith('/api/ai/')
+    }, { timeout: 1000 })
+    .then(() => true)
+    .catch(() => false)

   await messageInput.fill('this contains blocked')
   await page.getByTestId('chat-send-button').click({ force: true })

   await expect(page.getByText('不適切な内容が含まれています')).toBeVisible()
-  expect(aiRequests).toHaveLength(0)
+  expect(await aiRequestDetected).toBe(false)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const aiRequests: string[] = []
page.on('request', (request) => {
const url = new URL(request.url())
if (url.pathname.startsWith('/api/ai/')) {
aiRequests.push(url.pathname)
}
})
await messageInput.fill('this contains blocked')
await page.getByTestId('chat-send-button').click({ force: true })
await expect(page.getByText('不適切な内容が含まれています')).toBeVisible()
expect(aiRequests).toHaveLength(0)
})
const aiRequestDetected = page
.waitForRequest((request) => {
const url = new URL(request.url())
return url.pathname.startsWith('/api/ai/')
}, { timeout: 1000 })
.then(() => true)
.catch(() => false)
await messageInput.fill('this contains blocked')
await page.getByTestId('chat-send-button').click({ force: true })
await expect(page.getByText('不適切な内容が含まれています')).toBeVisible()
expect(await aiRequestDetected).toBe(false)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/kiosk-mode.spec.ts` around lines 89 - 102, The test currently
checks aiRequests immediately with expect(aiRequests).toHaveLength(0), which can
miss delayed network calls; replace this synchronous check with an explicit
wait-for-no-request using Playwright's waitForRequest on the page (targeting
paths starting with '/api/ai/') with a short timeout and assert that it times
out (or catch the timeout and treat it as success), or alternatively use
page.waitForRequest in a try/catch to fail if any matching request arrives
within the timeout; update the block around page.on('request', ...) and the
final assertion to use page.waitForRequest (or equivalent timeout-based logic)
to deterministically confirm no AI request was sent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants