OpenCodeMenuAppの技術仕様について説明します。
- バージョン: 1.0
- 更新日: 2026-01-20
OpenCodeMenuAppはmacOSメニューバーで動作するOpenCode API連携アプリケーションです。スクリーンショット取得と添付、AIとの対話、Mac操作の自動化をサポートします。
- macOS 14.0 以降
- Swift 5.9 以降
- Xcode 15.0 以降(またはSwift Toolchain)
- アクセシビリティ権限(スクリーンショット機能に必要)
- 設定:
.accessory - 説明: メニューバーのみで動作、Dockアイコンは表示されない
- 影響: ウィンドウ表示時に
NSApp.activate()を呼び出してフォーカスを取得する必要がある
- アイコン: macOSウィンドウアイコン(SF Symbol: "macwindow")
- 位置: システム時計の近く
- 動作: クリックでコンテキストメニューを表示
メニューバーアイコンをクリックすると以下のメニューが表示されます:
-
チャットウィンドウを表示 (Cmd+O)
- フローティングチャットウィンドウを表示
- 既に表示されている場合は前面に表示
-
入力ランチャーを表示 (Cmd+I)
- 入力ランチャーウィンドウを表示
-
再起動
- アプリケーションを再起動
NSWorkspace.shared.openApplication(at:configuration:)を使用
-
終了 (Cmd+Q)
- アプリケーションを終了
- ファイル名:
FloatingChatWindow.swift - サイズ: 初期値 340x600ピクセル
- リサイズ: 可能(
.resizableスタイル) - 位置: 画面左側(x: 20, y: 中央)
- ウィンドウレベル:
.floating(最前面表示) - スタイル:
.borderless(タイトルバーなし) - 背景色: 透明(不透明度15%)
- 動作:
- 表示時に
NSApp.activate()でアプリをアクティブ化 .canJoinAllSpacesで全スペースで表示.fullScreenAuxiliaryでフルスクリーン時に表示
- 表示時に
初期化:
super.init(
contentRect: NSRect(x: 20, y: 0, width: 340, height: 600),
styleMask: [.borderless, .resizable],
backing: .buffered,
defer: false
)表示フロー:
WindowStateManager.showChatWindow()が呼ばれるNSApp.activate(ignoringOtherApps: true)でアプリをアクティブ化orderFrontRegardless()でウィンドウを表示makeKey()でキーボードフォーカスを取得
- ファイル名:
InputLauncherWindow.swift - サイズ: 560x320ピクセル
- リサイズ: 不可
- 位置: 画面中央
- ウィンドウレベル:
.floating(最前面表示) - スタイル:
.borderless(タイトルバーなし) - 背景色: 透明
- 動作:
- 表示時に
NSApp.activate()でアプリをアクティブ化 - 送信成功時に自動クローズ
- キャンセルまたはESCでクローズ
- テキストフィールドに自動フォーカス(
@FocusState) - スクリーンショット添付時はテキストなしでも送信可能
- 表示時に
初期化:
super.init(
contentRect: NSRect(x: 0, y: 0, width: 560, height: 320),
styleMask: [.borderless],
backing: .buffered,
defer: false
)表示フロー:
WindowStateManager.showInputLauncher()が呼ばれる- チャットウィンドウが表示中の場合は非表示にする
NSApp.activate(ignoringOtherApps: true)でアプリをアクティブ化makeKeyAndOrderFront(nil)でウィンドウを表示- SwiftUIの
@FocusStateでテキストフィールドにフォーカス - 必要に応じてフォーカス要求通知を送信
| ショートカット | 機能 |
|---|---|
| Cmd+Shift+O | チャットウィンドウの表示/非表示切り替え |
| Cmd+Shift+I | 入力ランチャーの表示 |
| Shift+マウスドラッグ | 矩形選択スクリーンショット(入力ランチャーを表示) |
| ESC | 矩形選択キャンセル |
- ライブラリ: HotKey (https://github.com/soffes/HotKey)
- 監視:
GlobalShortcutMonitorクラス
チャットウィンドウショートカット:
let chatKeyCombo = KeyCombo(key: .o, modifiers: [.command, .shift])
chatWindowHotKey = HotKey(keyCombo: chatKeyCombo)
chatWindowHotKey?.keyDownHandler = { [weak self] in
self?.logStore.log("Cmd+Shift+O検出: チャットウィンドウ切り替え", category: "GlobalShortcut")
DispatchQueue.main.async {
self?.delegate?.didToggleChatWindow()
}
}入力ランチャーショートカット:
let inputKeyCombo = KeyCombo(key: .i, modifiers: [.command, .shift])
inputLauncherHotKey = HotKey(keyCombo: inputKeyCombo)
inputLauncherHotKey?.keyDownHandler = { [weak self] in
self?.logStore.log("Cmd+Shift+I検出: 入力ランチャー表示", category: "GlobalShortcut")
DispatchQueue.main.async {
self?.delegate?.didShowInputLauncher()
}
}スクリーンショット(Shift+マウスドラッグ)機能を使用するには、アクセシビリティ権限が必要です。
権限確認:
let options: [String: Any] = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
let trusted = AXIsProcessTrustedWithOptions(options as CFDictionary)権限なしの場合の処理:
- 起動時にアラートを表示
- システム環境設定へのパスを案内
- ファイル名:
WindowStateManager.swift - パターン: Singleton(
WindowStateManager.shared) - アクター:
@MainActor
責任:
- フローティングチャットウィンドウの表示/非表示管理
- 入力ランチャーの表示/非表示管理
- ウィンドウ間の排他制御
状態:
@Published private(set) var isChatWindowVisible = false主要メソッド:
setup(viewModel:)- ViewModelのセットアップshowChatWindow()- チャットウィンドウを表示hideChatWindow()- チャットウィンドウを非表示toggleChatWindow()- チャットウィンドウの切り替えshowInputLauncher()- 入力ランチャーを表示hideInputLauncher()- 入力ランチャーを非表示restartApp()- アプリケーションを再起動
排他制御:
- 入力ランチャーを表示する際、チャットウィンドウが表示中の場合は非表示にする
- チャットウィンドウと入力ランチャーを同時に表示しない
- ファイル名:
ScreenshotCapture.swift - API:
CGWindowListCreateImage(非推奨、将来ScreenCaptureKitに移行予定)
フロー:
- メインスクリーンのサイズを取得
CGWindowListCreateImageでキャプチャ- CGImageからNSImageへ変換
- PNG形式でエンコード
- Base64エンコード
- OpenCode APIに送信
- ファイル名:
ScreenshotRectCapture.swift - オーバーレイ:
ScreenSelectionOverlay.swift
フロー:
- Shiftキーが押されていることを確認
- マウスダウンでオーバーレイを表示
- マウスドラッグで矩形選択
- マウスアップで選択範囲をキャプチャ
- 入力ランチャーにスクリーンショットを添付
- テキストあり/なしで送信可能
最小サイズ: 50x50ピクセル
キャンセル:
- ESCキー
- 選択範囲が最小サイズ未満
- ファイル名:
RuntimeLogStore.swift - ログファイル:
~/github/chrome-to-opencode/opencode_app.log - 最大エントリ数: 500
ログレベル:
info- 通常の情報warning- 警告error- エラー
ログフォーマット:
2026-01-20 20:44:28.142 [INFO] [WindowManager] WindowStateManager初期化完了
カテゴリ:
App- アプリケーション全般Config- 設定管理WindowManager- ウィンドウ管理MenuBar- メニューバーGlobalShortcut- グローバルショートカットCapture- スクリーンショット取得Shortcut- ショートカットコールバックInputLauncherWindow- 入力ランチャーFloatingChatWindow- フローティングチャットウィンドウ
- API:
NSWorkspace.shared.openApplication(at:configuration:) - 完了ハンドラ: 新しいインスタンス起動後に既存のプロセスを終了
フロー:
- バンドルパスを取得
NSWorkspace.shared.openApplication()で新しいインスタンスを起動- 成功したら0.5秒待機
NSApplication.shared.terminate()で既存のプロセスを終了
コード:
let appUrl = URL(fileURLWithPath: appBundlePath)
let config = NSWorkspace.OpenConfiguration()
config.activates = true
NSWorkspace.shared.openApplication(at: appUrl, configuration: config) { app, error in
if let error = error {
RuntimeLogStore.shared.log("アプリ再起動エラー: \(error.localizedDescription)", level: .error, category: "WindowManager")
} else {
RuntimeLogStore.shared.log("アプリ起動成功", category: "WindowManager")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
RuntimeLogStore.shared.log("既存プロセスを終了します", category: "WindowManager")
NSApplication.shared.terminate(nil)
}
}
}-
CGWindowListCreateImageの非推奨
- ステータス: 非推奨(macOS 14.0以降)
- 影響: 現在は動作しているが、将来のバージョンで動作しなくなる可能性がある
- 対策: ScreenCaptureKitへの移行が必要
-
複数ディスプレイの部分的な対応
- ステータス: メインディスプレイのみ完全対応
- 影響: 複数ディスプレイ環境でのスクリーンショットはメインディスプレイのみ
- 対策: 将来的に全ディスプレイ対応を検討
-
ショートカットがトリガーされない場合がある
- ステータス: 報告あり
- 影響: Cmd+Shift+O/Iが反応しない場合がある
- 対策: 原因調査中
- ScreenCaptureKitへの移行
- ユニットテストの追加
- ショートカット問題の修正
- 複数ディスプレイの完全対応
- 複数セッションの管理
- セッション履歴の保存
- 自動更新機能
- プラグインシステム
- テーマのカスタマイズ