OpenCodeAppのアーキテクチャと設計について説明します。
OpenCodeAppはMVVM(Model-View-ViewModel)パターンを採用したmacOSメニューバーアプリケーションです。
graph TD
A[View] --> B[ViewModel]
B --> C[Service Layer]
C --> D[Model]
B --> D
style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#f0e1ff
style D fill:#e1ffe1
責任: ユーザーインターフェースの表示とユーザー入力の処理
ContentView.swift: メインビュー(SwiftUI)- メッセージ表示
- 入力フォーム
- セッションステータス表示
- ボタン操作
特性:
- SwiftUIで実装
@ObservedObjectでViewModelの変化を監視- UIのレンダリングとユーザーインタラクションのみを担当
責任: ビジネスロジックと状態管理
OpenCodeViewModel.swift: メインビューモデル- セッション管理
- メッセージ送信
- スクリーンショット添付/送信
- エラーハンドリング
特性:
@MainActorで実装@Publishedプロパティで状態管理- 非同期処理を担当
- ViewとModelの仲介役
責任: 外部API連携とデータ処理
-
OpenCodeAPIClient.swift: OpenCode API通信- セッション作成
- メッセージ送信
- エラーハンドリング
-
ScreenshotCapture.swift: スクリーンショット取得- 画面キャプチャ
- 画像処理
-
ConfigManager.swift: 設定管理- 設定ファイルの読み書き
- バリデーション
特性:
- 複数のServiceが存在
- 各Serviceは単一の責任を持つ
- ViewModelから呼び出される
責任: データ構造の定義
-
Config.swift: 設定データ構造- APIキー
- APIエンドポイント
- セッションタイムアウト
-
OpenCodeMessage.swift: メッセージデータ構造- メッセージID
- セッションID
- コンテンツ
- ロール(user/assistant)
- タイムスタンプ
-
OpenCodeSession.swift: セッションデータ構造- セッションID
- 作成日時
- 更新日時
- アクティブ状態
特性:
Codableプロトコルに準拠- データ検証ロジックを含む
責任: アプリケーションのライフサイクル管理
-
AppDelegate.swift: アプリデリゲート- アプリの初期化
- コンポーネントのセットアップ
- アプリの終了処理
-
MenuBarManager.swift: メニューバー管理- ステータスアイコンの表示
- ポップアップの管理
- 右クリックコンテキストメニューの表示
- イベントハンドリング
特性:
- NSApplicationDelegateに準拠
- アプリ全体の初期化を担当
- メニューバーアプリとしての動作を制御
責任: ウィンドウの管理と表示制御
-
FloatingChatWindow.swift: フローティングチャットウィンドウ- 独立したチャットインターフェースの提供
- リサイズ可能なウィンドウ管理
- フローティングレベルでの表示制御
-
InputLauncherWindow.swift: 入力ランチャーウィンドウ- 簡易入力インターフェースの提供
- 画面中央配置
- 送信後の自動クローズ制御
-
WindowStateManager.swift: ウィンドウ状態管理- フローティングチャットウィンドウの表示/非表示管理
- 入力ランチャーの表示/非表示管理
- ウィンドウ間の排他制御
特性:
- NSWindowを継承したカスタムウィンドウ
- SwiftUIのHostingViewを使用
- ウィンドウレベル(.floating)での表示
- スペース間の移動対応(.canJoinAllSpaces)
GlobalShortcutMonitor.swift: グローバルショートカット監視- Cmd+Shift+O: チャットウィンドウ切り替え
- Cmd+Shift+I: 入力ランチャー表示
- Shift+マウスドラッグ: 矩形選択スクリーンショット(入力ランチャーを表示)
- ESC: 選択キャンセル
- アクセシビリティ権限の管理
- HotKeyライブラリを使用したショートカット実装
ユーザーアクション (「セッション作成」ボタンクリック)
↓
ContentView (ボタンタップ)
↓
OpenCodeViewModel.createSession()
↓
OpenCodeAPIClient.createSession()
↓
OpenCodeAPIへのリクエスト
↓
OpenCodeAPIからのレスポンス
↓
OpenCodeSessionインスタンス生成
↓
ViewModelのcurrentSessionを更新 (@Published)
↓
ContentViewのUIが自動更新 (@ObservedObject)
ユーザーアクション (メッセージ入力 + Enter)
↓
ContentView (TextFieldのonSubmit)
↓
OpenCodeViewModel.sendMessage()
↓
OpenCodeAPIClient.sendMessage()
↓
OpenCodeAPIへのリクエスト (メッセージ)
↓
OpenCodeAPIからのレスポンス
↓
OpenCodeMessageインスタンス生成 (user + assistant)
↓
ViewModelのmessagesに追加 (@Published)
↓
ContentViewのUIが自動更新 (ScrollView)
ユーザーアクション (Shift+マウスドラッグ)
↓
GlobalShortcutMonitor.didCaptureRect()
↓
AppDelegate.handleRectCapture()
↓
ScreenshotRectCapture.captureRectAsData()
↓
OpenCodeViewModel.setPendingImageData()
↓
WindowStateManager.showInputLauncher()
↓
InputLauncherView (スクリーンショット添付表示)
↓
OpenCodeViewModel.sendLauncherPrompt()
↓
OpenCodeAPIClient.sendMessage()
↓
OpenCodeAPIからのレスポンス
↓
ViewModelのmessagesに追加 (@Published)
↓
FloatingChatViewのUIが自動更新
ユーザーアクション (Cmd+Shift+O または 右クリックメニュー)
↓
GlobalShortcutMonitor.didToggleChatWindow() または MenuBarManager.showChatWindow()
↓
WindowStateManager.toggleChatWindow() (@MainActor)
↓
チャットウィンドウの表示/非表示切り替え
↓
NSApp.activate()でアプリをアクティブ化
↓
isChatWindowVisibleの更新 (@Published)
ユーザーアクション (Cmd+Shift+I または 右クリックメニュー)
↓
GlobalShortcutMonitor.didShowInputLauncher() または MenuBarManager.showInputLauncher()
↓
WindowStateManager.showInputLauncher() (@MainActor)
↓
チャットウィンドウが表示中の場合は非表示
↓
NSApp.activate()でアプリをアクティブ化
↓
入力ランチャーを表示
↓
画面中央に配置
↓
@FocusStateでテキストフィールドにフォーカス
SwiftのARCによってメモリ管理が自動化されていますが、以下の点に注意が必要です。
OpenCodeViewModelでは、Combineのサブスクリプションを適切に管理します:
@MainActor
class OpenCodeViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
init(apiClient: OpenCodeAPIClient, screenshotCapture: ScreenshotCapture) {
self.apiClient = apiClient
self.screenshotCapture = screenshotCapture
}
// cancellablesはdeinit時に自動的にキャンセルされる
}非同期処理でselfをキャプチャする際は、weak参照を使用します:
Task {
await apiClient.createSession()
// ViewModel自体は@MainActorで管理されるため、強参照は問題ない
// ただし、将来的にコールバックを使用する場合はweak selfを考慮
}スクリーンショットの画像データは、処理完了後に適切に解放されます:
func captureScreenAsData() throws -> Data {
let image = try captureScreen()
let imageData = try convertToPNG(image)
return imageData // DataはARCで管理され、不要になると解放される
}URLSessionの使用は、リクエスト完了後に自動的にクリーンアップされます:
let (data, response) = try await session.data(for: request)
// データとレスポンスはスコープ外で自動的に解放されるSwift 5.5+のasync/awaitを全面的に採用しています:
func createSession() async throws -> OpenCodeSession {
let (data, response) = try await session.data(for: request)
return try JSONDecoder().decode(OpenCodeSession.self, from: data)
}UI操作は@MainActorで実行されます:
@MainActor
class OpenCodeViewModel: ObservableObject {
@Published var currentSession: OpenCodeSession?
func createSession() async {
isLoading = true
do {
let session = try await apiClient.createSession()
currentSession = session // UI更新はMainActorで行われる
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}非同期処理でのエラーハンドリング:
do {
let session = try await apiClient.createSession()
currentSession = session
} catch APIError.unauthorized {
errorMessage = "認証に失敗しました"
} catch {
errorMessage = "予期しないエラー: \(error.localizedDescription)"
}- APIキーは
.config.jsonに保存 .config.jsonは.gitignoreに含まれる- リポジトリにはコミットされない
- HTTPSを使用(
https://api.opencode.ai) - 認証にはBearer Tokenを使用
- タイムアウト設定(30秒)
- スクリーンショット取得には権限は不要(システムAPI使用)
- 将来的にはScreenCaptureKitへの移行が必要
- ScreenCaptureKitへの移行: CGWindowListCreateImageは非推奨
- ユニットテストの追加: 各コンポーネントのテストカバレッジ向上
- ロギングの追加: デバッグとトラブルシューティングの強化
- エラーリカバリー: ネットワークエラー時のリトライロジック
- パフォーマンス最適化: 大量メッセージ時のパフォーマンス改善