Skip to content

feat: add Dangerous Read Everything Mode with AX tree traversal#65

Merged
chigichan24 merged 34 commits into
mainfrom
feature/dangerous-read-everything-mode
Apr 7, 2026
Merged

feat: add Dangerous Read Everything Mode with AX tree traversal#65
chigichan24 merged 34 commits into
mainfrom
feature/dangerous-read-everything-mode

Conversation

@chigichan24
Copy link
Copy Markdown
Owner

Summary

  • macOS Accessibility API(AXUIElement)を使ってフォーカス中ウィンドウのテキストを定期的に読み取り、LLMのコンテキストとして注入する「Dangerous Read Mode」を追加
  • ウィンドウ内のAXツリーを再帰走査し、全テキストを位置ベースでソートして収集(Slackのチャット履歴等もコンテキストに含まれる)
  • モードアクティブ中はパクパク(ᗧ···)アニメーション付きの赤いインジケーターバッジを表示

安全機構(7重の防御)

  1. Settings Gate — デフォルト無効、設定で明示的に有効化が必要
  2. Accessibility Trust CheckAXIsProcessTrusted()でシステムレベルの権限確認
  3. NSAlert同意ダイアログ — 毎セッション開始時に.criticalスタイルで確認
  4. 時間制限 — 設定可能な最大セッション時間(デフォルト5分)で自動停止
  5. 視覚インジケーター — 赤いフローティングバッジで残り時間カウントダウン
  6. Ctrl+Shift+D — 即座にモード停止
  7. テキスト量制限maxScreenContextLengthで送信テキスト量を制限

新規ファイル

  • Hatoko/Accessibility/ — AccessibilityPermission, ScreenContext, ScreenReader (actor), DangerousReadModeController
  • Hatoko/UI/DangerousReadIndicatorWindow.swift — パクパクアニメーション付きインジケーター
  • Hatoko/InputMethod/HatokoInputController+LLMGeneration.swift — LLM生成ロジック分離

Test plan

  • mint run swiftlint lint --strict — 0 violations
  • xcodebuild test — 全テスト合格
  • install.sh → System Settingsでアクセシビリティ権限付与
  • 設定画面でDangerous Read Mode有効化 → 権限状態が正しく表示される
  • Ctrl+Shift+D → 同意ダイアログ表示 → パクパクインジケーター表示
  • テキストエディタ/Slackでテキスト表示中にCtrl+Space → LLMプロンプト → 画面コンテキストが応答に反映
  • 5分後に自動停止 + ビープ音
  • Ctrl+Shift+Dで即座に停止

🤖 Generated with Claude Code

chigichan24 and others added 30 commits April 7, 2026 05:07
Introduces the foundation types for the Dangerous Read Mode feature:
- ScreenContext: Sendable struct for captured screen data with XML formatting
- AccessibilityPermission: AXIsProcessTrusted wrapper and trust request helper
- PromptGuard.maxScreenContextLength constant (4000 chars)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Swift actor that captures screen context via macOS Accessibility API:
- Reads focused app name, window title, focused text, selected text
- Skips capture when Hatoko itself is focused
- Graceful AX error handling with nil returns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests for ScreenContext struct construction, formatting, truncation,
and PasteContext.buildSystemPrompt integration with screenContext parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- PasteContext.buildSystemPrompt gains screenContext: parameter (default nil)
- Screen context injected as <screen_context> XML block after paste context
- SystemPromptProvider.screenContextInstruction added for LLM guidance
- LLM generation logic extracted to HatokoInputController+LLMGeneration.swift

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- DangerousReadModeController: session lifecycle with 7-layer safety
  (settings gate, AX trust, consent dialog, time limit, visual indicator,
  instant kill shortcut, text truncation)
- DangerousReadIndicatorWindow: red floating badge with countdown timer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Settings section with enable toggle, duration/interval pickers,
  accessibility permission check, and red warning text
- Keybinding hint for Ctrl+Shift+Space added
- Localization strings for en and ja

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Ctrl+Shift+Space toggles dangerous read mode session
- Ctrl+Space now excludes Shift to avoid conflict
- Marked text prefix shows warning emoji when mode is active
- LLM generation extracted to separate extension for file length

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… truth

Move key strings and default values from private constants in
DangerousReadModeController to public static lets. SettingsView now
references these constants instead of duplicating magic strings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add activeState check before writing capture results to prevent
a theoretical race where a pre-cancellation capture overwrites
the nil set by stopSession().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents Task leak when IMKInputController instances are recreated
during IME activation/deactivation cycles while a session is active.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Eliminates TOCTOU risk by combining the active check and context read
into a single atomic operation. Also makes latestScreenContext private.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
remainingSeconds now uses the duration captured at session start
instead of re-reading UserDefaults, preventing inconsistency if
settings change during an active session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
storedMaxDuration() and storedCaptureInterval() centralize the
"read from UserDefaults, fallback to default" logic that was
previously duplicated in both DangerousReadModeController and SettingsView.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Truncates windowTitle to 500 chars to prevent unexpectedly long
titles (e.g. browser tabs with full URLs) from inflating the
LLM system prompt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Chat responses should only be added to the chat window, not stored
in llmSuggestion which is used by the inline suggestion flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…o Ctrl+Shift+D

Avoids conflict with Ctrl+Space (LLM mode). Restores original
isCtrlSpace check without shift exclusion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When visibleText (from AX tree traversal) is available, formatted()
outputs it as "Visible text:" and omits focusedText to avoid
redundancy. Falls back to focusedText when visibleText is nil.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
captureFullWindowContext() walks the focused window's AX element tree
recursively, collecting all visible text sorted top-to-bottom by
position. Deduplicates parent-child text overlap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switches capture loop to captureFullWindowContext() with scanning
state signaling. Indicator uses @observable state to drive pac-man
animation (ᗧ···) during tree traversal at 200ms frame intervals.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- scanFrame is now private(set) with advanceScanFrame/resetScanFrame
  methods to prevent out-of-bounds array access
- View uses modulo on access as additional safety
- Added doc comment explaining why @unchecked Sendable is needed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Caps collected text fragments at 500 to prevent memory exhaustion
on apps with extremely large AX trees (e.g. complex browser pages).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds CFGetTypeID guard before force casting to AXValue, matching
the defensive pattern used in copyElementAttribute.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers emptyVisibleTextTreatedAsNil and truncatesLongWindowTitle
to complete ScreenContext field validation coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…conds

- defer guarantees setScanning(false) on early return from capture loop
- startCountdown now uses remainingSeconds computed property as single
  source of truth instead of an independent decrementing counter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- setScanning(true/false) now bracket the capture call within each
  loop iteration instead of using defer at Task scope
- startSession() returns early if already active to prevent Task leak

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All ScreenContext fields now have length limits, preventing
unexpectedly long app names from inflating the LLM prompt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes the misleading do-without-catch block. defer guarantees
setScanning(false) on every exit path including early return and
Task cancellation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
captureFullWindowContext() supersedes this method. Removing dead
code eliminates the risk of the two methods diverging silently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers the truncateShort applied to appName added in the previous
fix, ensuring all fields have consistent test coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- setScanning(false) now called immediately after capture, not via
  defer at while-scope which left indicator on during sleep interval
- Task-level defer ensures cleanup on cancellation/early return
- nil capture results no longer overwrite valid previous context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
chigichan24 and others added 4 commits April 7, 2026 06:21
Clarifies that only consecutive elements are compared for containment,
not all pairs. Adds doc comment explaining the behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per Swift Concurrency best practices, long-running Tasks should
capture self weakly to avoid circular references (Task holds self,
self holds Task via captureTask/countdownTask properties).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Permission check was evaluated once at view init and never updated.
Now re-checks on button tap (with 1s delay for system dialog) and
on every settings window appearance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The munching animation now runs the entire time Dangerous Read Mode
is on, not just during AX tree traversal. More fun and clearer
visual feedback that the mode is active.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@chigichan24 chigichan24 merged commit 39fd3d1 into main Apr 7, 2026
2 checks passed
@chigichan24 chigichan24 deleted the feature/dangerous-read-everything-mode branch April 21, 2026 17:00
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.

1 participant