Home

2026-01-26 バックグラウンド通知設計

検討: バックグラウンド通知設計(Gemini Live + Web Notification)

背景

Gemini Live APIを使った音声UI実装において、ユーザーが別アプリを使用中でもタスク完了を確実に通知する必要がある。

利用シーン

[音声指示 30秒] → [待機 5-30分] → [完了通知] → [確認/次の指示]
                     ↓
              他のアプリで作業中
              エディタ使用中
              ブラウザがバックグラウンド

採用方針: Web Notification + 効果音 + Gemini Live音声

基本フロー

[バックグラウンドでタスク完了]
         ↓
🔔 Web Notification: 「Claude Code - タスク完了」
🔊 効果音: ピロン♪(短い通知音)
         ↓
[ユーザーが通知をクリック]
         ↓
[アプリ画面にフォーカス]
         ↓
🎤 Gemini Live: 詳細を音声で説明
「実装が完了しました。backend/internal/service/claude.goに
新しいストリーミング機能を追加し、3つのテストケースを
追加しています。ビルドとテストは全て成功しました。」

通知タイミングの方針

イベントWeb通知効果音Gemini音声備考
14分再接続静かに実行(技術的制約なのでユーザーは気にしなくていい)
タスク完了フォーカス後に音声説明
エラー発生フォーカス後に音声説明

実装設計

1. Web Notification API の実装

許可リクエスト

// frontend/src/hooks/useNotification.ts

export function useNotification() {
  const [permission, setPermission] = useState(Notification.permission);

  // アプリ起動時に許可をリクエスト
  useEffect(() => {
    if (permission === "default") {
      Notification.requestPermission().then(setPermission);
    }
  }, []);

  return { permission };
}

通知表示

// frontend/src/lib/notification.ts

interface NotificationOptions {
  title: string;
  body: string;
  icon?: string;
  soundUrl?: string;  // 効果音のURL
  onFocus?: () => void;  // フォーカス時のコールバック
}

export async function showNotificationWithSound(options: NotificationOptions) {
  const { title, body, icon = "/icon.png", soundUrl, onFocus } = options;

  // 1. 許可チェック
  if (Notification.permission !== "granted") {
    console.warn("Notification permission not granted");
    return;
  }

  // 2. ブラウザ通知を表示
  const notification = new Notification(title, {
    body,
    icon,
    requireInteraction: true,  // クリックまで消えない
    tag: "claude-code-task",   // 重複防止
    silent: true  // ブラウザのデフォルト音を消す
  });

  // 3. 効果音を再生
  if (soundUrl) {
    try {
      const audio = new Audio(soundUrl);
      await audio.play();
    } catch (error) {
      console.error("Failed to play notification sound:", error);
    }
  }

  // 4. クリックでアプリにフォーカス
  notification.onclick = () => {
    window.focus();
    notification.close();
    onFocus?.();  // Gemini音声のトリガー
  };

  // 5. 自動クローズ(30秒後)
  setTimeout(() => notification.close(), 30000);
}

2. Gemini Live 音声説明の実装

タスク完了時の音声生成

// frontend/src/hooks/useGeminiLive.ts

interface TaskCompletionInfo {
  status: "success" | "error";
  summary: string;  // バックエンドから受け取る概要
  details?: {
    filesChanged: string[];
    testsRun?: number;
    testsPassed?: number;
    errorMessage?: string;
  };
}

export function useGeminiLive() {
  const [ws, setWs] = useState<WebSocket | null>(null);
  const [isConnected, setIsConnected] = useState(false);

  // タスク完了時に呼び出す
  async function announceTaskCompletion(info: TaskCompletionInfo) {
    if (!ws || !isConnected) {
      console.warn("Gemini Live not connected");
      return;
    }

    // システム指示に完了情報を含めて音声生成
    const prompt = buildCompletionPrompt(info);

    // Gemini Liveに送信
    ws.send(JSON.stringify({
      client_content: {
        turns: [{
          role: "user",
          parts: [{ text: prompt }]
        }]
      }
    }));
  }

  return {
    announceTaskCompletion,
    isConnected
  };
}

function buildCompletionPrompt(info: TaskCompletionInfo): string {
  if (info.status === "success") {
    return `
タスクが完了しました。以下の内容を簡潔に音声で説明してください。

概要: ${info.summary}

変更ファイル:
${info.details?.filesChanged.map(f => `- ${f}`).join('\n')}

${info.details?.testsRun ? `テスト実行: ${info.details.testsPassed}/${info.details.testsRun} 成功` : ''}

30秒以内で、ユーザーが次に何をすべきかを含めて説明してください。
`;
  } else {
    return `
エラーが発生しました。以下の内容を簡潔に音声で説明してください。

エラー概要: ${info.summary}

${info.details?.errorMessage ? `詳細: ${info.details.errorMessage}` : ''}

30秒以内で、解決方法の提案を含めて説明してください。
`;
  }
}

3. 統合実装

page.tsx での統合

// frontend/src/app/page.tsx

export default function Home() {
  const { permission } = useNotification();
  const { announceTaskCompletion, isConnected } = useGeminiLive();
  const [shouldAnnounce, setShouldAnnounce] = useState<TaskCompletionInfo | null>(null);

  // SSEストリームの完了イベント
  const handleStreamComplete = useCallback((result: TaskCompletionInfo) => {
    // 1. Web通知 + 効果音
    showNotificationWithSound({
      title: "Claude Code",
      body: result.status === "success"
        ? "タスクが完了しました"
        : "エラーが発生しました",
      soundUrl: result.status === "success"
        ? "/notification-success.mp3"
        : "/notification-error.mp3",
      onFocus: () => {
        // 2. フォーカス後にGemini音声をトリガー
        setShouldAnnounce(result);
      }
    });
  }, []);

  // アプリにフォーカスが戻ったらGemini音声を再生
  useEffect(() => {
    if (shouldAnnounce && isConnected) {
      announceTaskCompletion(shouldAnnounce);
      setShouldAnnounce(null);
    }
  }, [shouldAnnounce, isConnected]);

  return (
    <div>
      {/* 通知許可のリクエストUI */}
      {permission === "default" && (
        <div className="notification-banner">
          タスク完了通知を受け取るには、ブラウザの通知を許可してください。
        </div>
      )}

      {/* 既存のUI */}
    </div>
  );
}

4. バックエンドからの完了情報

SSEイベントに完了情報を追加

// backend/internal/service/claude.go

type TaskCompletionInfo struct {
    Status       string   `json:"status"`  // "success" or "error"
    Summary      string   `json:"summary"`
    FilesChanged []string `json:"files_changed,omitempty"`
    TestsRun     int      `json:"tests_run,omitempty"`
    TestsPassed  int      `json:"tests_passed,omitempty"`
    ErrorMessage string   `json:"error_message,omitempty"`
}

type StreamEvent struct {
    Type       string               `json:"type"`
    SessionID  string               `json:"session_id"`
    Message    string               `json:"message"`
    Completion *TaskCompletionInfo  `json:"completion,omitempty"`  // 追加
}

func (s *ClaudeService) StreamCommand(ctx context.Context, input string) (<-chan StreamEvent, error) {
    eventCh := make(chan StreamEvent)

    go func() {
        defer close(eventCh)

        // Claude CLI実行
        output, err := s.executeClaude(ctx, input)

        if err != nil {
            // エラー時
            eventCh <- StreamEvent{
                Type:    "completion",
                Message: "Error occurred",
                Completion: &TaskCompletionInfo{
                    Status:       "error",
                    Summary:      parseErrorSummary(err),
                    ErrorMessage: err.Error(),
                },
            }
            return
        }

        // 成功時
        eventCh <- StreamEvent{
            Type:    "completion",
            Message: "Task completed",
            Completion: &TaskCompletionInfo{
                Status:       "success",
                Summary:      parseSuccessSummary(output),
                FilesChanged: extractChangedFiles(output),
                TestsRun:     extractTestsRun(output),
                TestsPassed:  extractTestsPassed(output),
            },
        }
    }()

    return eventCh, nil
}

ブラウザ制限への対応

バックグラウンドでの音声再生制限

問題点:

  • Chrome/Safari: タブがバックグラウンドの場合、音声再生が制限される
  • 解決策: 通知クリック後に音声を再生(採用方針)

実装のポイント:

// ❌ バックグラウンドで音声再生(失敗する可能性)
showNotification();
playGeminiAudio();  // 制限される

// ✅ フォーカス後に音声再生(確実)
showNotification({
  onFocus: () => playGeminiAudio()  // ユーザーアクション後なので再生可能
});

効果音ファイルの準備

推奨仕様

項目
フォーマットMP3
サイズ10KB以下
長さ0.5-1秒
配置場所/public/notification-success.mp3
/public/notification-error.mp3

無料素材の例

推奨音:

  • 成功: 明るいピロン音、ベル音
  • エラー: 控えめなビープ音

実装の優先順位

フェーズ1: 基本通知(MVP)

  1. Web Notification API の実装
  2. 効果音の再生
  3. 許可リクエストUI

完成イメージ:

[バックグラウンド]
🔔 「タスク完了」
🔊 ピロン♪

フェーズ2: Gemini Live 統合

  1. Gemini Live WebSocket接続
  2. タスク完了時の音声生成
  3. フォーカス後の自動再生

完成イメージ:

[通知クリック]
         ↓
🎤 「実装が完了しました。3つのファイルを変更しています...」

フェーズ3: 高度な機能

  1. 音声内容のカスタマイズ
  2. 通知履歴の管理
  3. 通知設定のカスタマイズUI

トレードオフ

メリット

  1. 確実な通知

    • Web Notificationで視覚的に通知
    • 効果音で聴覚的にも通知
    • ブラウザ標準機能で安定動作
  2. バックグラウンド制限を回避

    • フォーカス後にGemini音声再生
    • ユーザーアクション後なので確実
  3. 段階的実装が可能

    • フェーズ1だけでも価値がある
    • Gemini Liveは後から追加可能

デメリット

  1. 初回許可が必要

    • ユーザーが通知を許可しないと動作しない
    • 許可リクエストUIが必要
  2. タブを閉じると動作しない

    • ブラウザタブが開いている必要がある
    • → 実用上は問題ない(アプリを使用中)
  3. 音声は通知クリック後

    • 通知を無視すると音声が流れない
    • → 効果音で気づかせるので軽減

セキュリティとプライバシー

通知内容の制限

通知テキスト:

  • 機密情報を含めない
  • 「タスク完了」「エラー発生」のみ

音声説明:

  • アプリにフォーカス後のみ再生
  • ファイル名や詳細はアプリ画面内で表示

次のステップ

1. プロトタイプ実装(フェーズ1)

  • useNotification フックの実装
  • showNotificationWithSound 関数の実装
  • 効果音ファイルの配置
  • page.tsx での統合

2. バックエンド対応

  • StreamEventCompletion フィールド追加
  • ログからファイル変更を抽出
  • テスト結果を抽出

3. Gemini Live 統合(フェーズ2)

  • WebSocket接続の実装
  • 音声生成プロンプトの作成
  • フォーカス後の自動再生

参考資料