Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/architecture/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ description: VimAction 프로젝트의 현재 아키텍처·구조(최종 상태
| 포커스 리졸버·디스패치 읽기 | 계열 캐시·갱신 트리거, 게시 큐 위 창 읽기(`FocusedText`)와 파생 질의·소비 지점 | [focus-and-dispatch-reads.md](references/focus-and-dispatch-reads.md) |
| 재진입과 안전장치 | 합성 이벤트 마커 불변식(무한 루프 방지), 단일 ActionExecutor, 킬스위치·중단 래치·원자 그룹 | [reentrancy-and-safety.md](references/reentrancy-and-safety.md) |
| 프로파일과 설정 | YAML(Yams) 설정 시딩·스키마, 앱별 프로파일, 수동 리로드, UserDefaults 경계 | [profiles-and-config.md](references/profiles-and-config.md) |
| 앱 셸 | 메뉴바·설정 창 3탭·Dock 아이콘 수명·로그인 자동 시작·Sparkle·온보딩 push | [app-shell.md](references/app-shell.md) |
| 앱 셸 | 메뉴바·설정 창 4탭·Dock 아이콘 수명·로그인 자동 시작·Sparkle·온보딩 push | [app-shell.md](references/app-shell.md) |
| 온스크린 모드 인디케이터 | 모드 전환 시 오버레이 라벨 — 콜백 훅·전용 읽기 큐·앵커 사다리(요소→창)·순수 레이아웃·비활성화 패널·메뉴바 표시 사다리 재사용 | [mode-indicator-overlay.md](references/mode-indicator-overlay.md) |
6 changes: 3 additions & 3 deletions .claude/skills/architecture/references/app-shell.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 앱 셸

- **Last updated**: 2026-09-04 (릴리스 노트 노출 배선 반영)
- **Last updated**: 2026-09-15 (설정 창 Indicator 탭 — 4탭)

## 현재 구조

Expand All @@ -18,9 +18,9 @@

열림 훅의 앱측 진입점은 `AppState.settingsWindowDidAppear()`이고 reader와 함께 **`TabView` 루트에 붙는다** — 탭 하나 안에 두면 다른 탭이 기본으로 열릴 때 훅이 뜨지 않는다. 이 메서드는 Dock 훅·온보딩 마무리·로그인 항목 상태 재조회를 겸하므로 **멱등**이다.

### 설정 창 — `TabView` 3탭, 460×560 고정
### 설정 창 — `TabView` 4탭, 460×560 고정

**General**(권한 상태 + Behavior[로그인 시 자동 시작·Normal 탈출·온스크린 모드 인디케이터] + Updates 토글 + 접힌 `DisclosureGroup` "Diagnostics") / **Apps**(설정 파일 상태 — [profiles-and-config.md](profiles-and-config.md)) / **About**(버전 + 업데이트 확인 + 링크). 탭 순서는 General이 먼저지만 **기본 선택은 Apps**이고, 권한 섹션은 미허용이면 General 최상단·허용되면 아래쪽(Behavior·Updates 뒤)에 한 줄로 자리를 옮긴다. 높이를 명시하지 않으면 greedy한 grouped `Form` 때문에 창이 임의 높이로 굳는다 ([20260809_settings-window-three-tabs.md](../../decisions/references/20260809_settings-window-three-tabs.md)).
**General**(권한 상태 + Behavior[로그인 시 자동 시작·Normal 탈출] + Updates 토글 + 접힌 `DisclosureGroup` "Diagnostics") / **Indicator**(탭 아이콘 `text.cursor`, 섹션 하나 — 온스크린 모드 인디케이터 토글과 그 아래 "Show" Picker(전환 시만 / Normal·Visual 동안도 — 토글이 꺼지면 `.disabled`)·"Indicator style" Picker(배지 / 창 테두리 / 화면 테두리 — 토글이 꺼지거나 상시 표시가 없으면 `.disabled`), 값·영속은 `ModeIndicatorController` 소유이고 뷰는 바인딩만, [mode-indicator-overlay.md](mode-indicator-overlay.md)) / **Apps**(설정 파일 상태 — [profiles-and-config.md](profiles-and-config.md)) / **About**(버전 + 업데이트 확인 + 링크). 탭 순서는 General이 먼저지만 **기본 선택은 Apps**이고, 권한 섹션은 미허용이면 General 최상단·허용되면 아래쪽(Behavior·Updates 뒤)에 한 줄로 자리를 옮긴다. 높이를 명시하지 않으면 greedy한 grouped `Form` 때문에 창이 임의 높이로 굳는다 ([20260809_settings-window-three-tabs.md](../../decisions/references/20260809_settings-window-three-tabs.md), [20260915_settings-window-indicator-tab.md](../../decisions/references/20260915_settings-window-indicator-tab.md)).

### 로그인 시 자동 시작 — `SMAppService.mainApp`

Expand Down
70 changes: 40 additions & 30 deletions .claude/skills/architecture/references/mode-indicator-overlay.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions .claude/skills/architecture/references/profiles-and-config.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 프로파일과 설정

- **Last updated**: 2026-08-14 (M5 D2 — 번들 기본 전략 `keyboard` → `auto` 전환 완료, 도그푸딩 게이트 통과 뒤 별도 커밋)
- **Last updated**: 2026-09-06 (온스크린 인디케이터 스타일 키 추가)

## 현재 구조

Expand Down Expand Up @@ -96,7 +96,7 @@ actions: # 명령 계열: 그 액션 자신의 키 교

**"사용자가 파일로 관리하고 싶은 설정"은 YAML, "앱이 스스로 쓰는 상태"는 UserDefaults.**

- UserDefaults 잔류: `interceptionEnabled`(마스터 토글 — 킬스위치가 전용 스레드에서 직접 영속하는 안전 경로라 파일 IO에 의존시키지 않는다), `normalModeEscapeEnabled`(기존 Settings UI 토글 유지), `onScreenModeIndicatorEnabled`(온스크린 모드 인디케이터 on/off, 기본 on — 표시 취향이지 앱별 동작 정의가 아니고, YAML로 두면 Settings UI가 YAML을 써야 한다. 소유자는 `ModeIndicatorController` — [20260906_mode-indicator-settings-in-userdefaults.md](../../decisions/references/20260906_mode-indicator-settings-in-userdefaults.md)).
- UserDefaults 잔류: `interceptionEnabled`(마스터 토글 — 킬스위치가 전용 스레드에서 직접 영속하는 안전 경로라 파일 IO에 의존시키지 않는다), `normalModeEscapeEnabled`(기존 Settings UI 토글 유지), `onScreenModeIndicatorEnabled`(온스크린 모드 인디케이터 on/off, 기본 on)·`onScreenModeIndicatorPersistentEnabled`(상시 표시 on/off — off면 순간 표시만, 기본 on)·`onScreenModeIndicatorStyle`(상시 표시 스타일 — `ModeIndicatorPresentationStyle`의 raw 문자열 `badge`/`windowBorder`/`screenBorder`, 기본 `badge`, 없는 값·모르는 값은 기본) — 셋 다 표시 취향이지 앱별 동작 정의가 아니고, YAML로 두면 Settings UI가 YAML을 써야 한다. 소유자는 `ModeIndicatorController` ([20260906_mode-indicator-settings-in-userdefaults.md](../../decisions/references/20260906_mode-indicator-settings-in-userdefaults.md), [20260914_mode-indicator-optin-flash-only-and-window-border.md](../../decisions/references/20260914_mode-indicator-optin-flash-only-and-window-border.md)).
- YAML: 앱별 on/off, 프로파일 전부.
- **어느 쪽도 아닌 셋째 칸이 있다 — 시스템이 소유하는 상태.** 로그인 시 자동 시작은 `SMAppService.mainApp.status`가 SSOT이고 앱은 값을 저장하지 않는다(사용자가 시스템 설정에서 통지 없이 바꾸므로 미러는 반드시 어긋난다 — [app-shell.md](app-shell.md)). Sparkle 자동 확인도 같은 성격으로, 값의 영속은 Sparkle이 소유한다.

Expand Down
4 changes: 3 additions & 1 deletion .claude/skills/decisions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ description: VimAction 프로젝트의 기술 결정 히스토리 SSOT — 아
| 08-02 | App Icon은 Icon Composer `.icon` | legacy는 macOS 26이 회색 컨테이너에 강제 합성(실측) — appiconset 제거, 소스 벡터는 stroke가 아닌 외곽선 fill이어야 함 (26.5 전제는 20260812 배포 타깃 결정으로 부분 supersede) | [20260802_app-icon-icon-composer.md](references/20260802_app-icon-icon-composer.md) |
| 08-02 | 설정 창 열린 동안만 Dock 아이콘 | `.regular` 승격(앱 메뉴 동반 수용) — 열림=`onAppear`(창이 key가 안 됨, 실측)·닫힘=`willClose` (닫힘 판정 술어는 20260812 창 동일성으로 부분 supersede) | [20260802_dock-icon-while-settings-open.md](references/20260802_dock-icon-while-settings-open.md) |
| 08-09 | UI 쓰기는 라인 편집만 | 재직렬화 금지는 유지, 값 토큰 교체·한 줄 삽입은 허용 — 안전 계약 3종(에러 상태 거부/애매하면 nil+결과 자가검증/파일 열기 폴백) | [20260809_config-yaml-line-edit-writes.md](references/20260809_config-yaml-line-edit-writes.md) |
| 08-09 | 설정 창은 3탭 | General(권한·Behavior·접힌 Diagnostics)/Apps(기본 선택)/About, 460×560 고정 — 권한 섹션은 신뢰 상태에 따라 자리 이동 | [20260809_settings-window-three-tabs.md](references/20260809_settings-window-three-tabs.md) |
| 08-09 | 설정 창은 3탭 | General(권한·Behavior·접힌 Diagnostics)/Apps(기본 선택)/About, 460×560 고정 — 권한 섹션은 신뢰 상태에 따라 자리 이동 (탭 수는 20260915 Indicator 탭으로 부분 supersede) | [20260809_settings-window-three-tabs.md](references/20260809_settings-window-three-tabs.md) |
| 08-09 | 미허용 발견성은 push 2종 | 런치 1회 자동 오픈(`openSettings`를 `MenuBarExtra` label에서 — `sendAction`은 macOS 14부터 사망) + 메뉴 상시 경고 행 (26.5 전제는 20260812 배포 타깃 결정으로 부분 supersede — 결론 유지) | [20260809_permission-discoverability-push.md](references/20260809_permission-discoverability-push.md) |
| 08-10 | 자동 업데이트는 Sparkle 2.x | appcast + EdDSA·코드서명 이중 검증, 샌드박스 해제라 XPC 불필요 — 자체 구현 기각 | [20260810_sparkle-auto-update.md](references/20260810_sparkle-auto-update.md) |
| 08-10 | 배포 패키징은 DMG | 첫 설치 UX·설치 위치 안정성 — Sparkle·cask 공용, zip 기각 | [20260810_dmg-packaging.md](references/20260810_dmg-packaging.md) |
Expand Down Expand Up @@ -102,6 +102,8 @@ description: VimAction 프로젝트의 기술 결정 히스토리 SSOT — 아
| 09-06 | 앵커 사다리 — 캐럿→요소→창, 이벤트 기반 갱신 | 순간 표시는 캐럿부터·상시 배지는 요소부터, 키마다 재배치·폴링 없음(20260725 재검토 트리거 회피), AX는 메인 밖 50ms·pid만, 캐럿은 `(loc,1)`→`(loc-1,1)`→텍스트 마커 — 앱 계열별 실측표 포함 | [20260906_mode-indicator-anchor-ladder-event-driven.md](references/20260906_mode-indicator-anchor-ladder-event-driven.md) |
| 09-06 | Chromium 스크린리더 모드 강제 안 함 | `AXEnhancedUserInterface`를 켜면 Chrome·Arc 캐럿이 나옴을 실측했으나 브라우저 전역 완전 모드 비용이 비례하지 않음 — 캐럿 없음 수용·요소 폴백, Electron `AXManualAccessibility` 기상은 유지 | [20260906_no-forced-chromium-screen-reader-mode.md](references/20260906_no-forced-chromium-screen-reader-mode.md) |
| 09-06 | 인디케이터 설정은 UserDefaults | on/off·스타일은 Settings General 토글(20260801 경계 기준), config.yaml 비노출, 기본 on — PRD 초안의 YAML 스타일 항목 대체 | [20260906_mode-indicator-settings-in-userdefaults.md](references/20260906_mode-indicator-settings-in-userdefaults.md) |
| 09-14 | 인디케이터 opt-in 확장 — 순간 표시만 + 창 테두리 | 상시 표시 on/off Bool 키 추가(off면 flash만), 상시 스타일에 포커스 창 테두리(폴백 없음) 추가, 기본값 불변 — 3단 enum 기각(마이그레이션), 자기 pid 경로는 떠 있는 읽기 무효화·flash 만료 | [20260914_mode-indicator-optin-flash-only-and-window-border.md](references/20260914_mode-indicator-optin-flash-only-and-window-border.md) |
| 09-15 | 설정 창에 Indicator 탭 | General / Indicator / Apps / About 4탭 — 인디케이터 항목 셋을 General > Behavior에서 옮기기만(키·문구 불변), "Visual" 이름·About 앞 배치 기각, 08-09 3탭 결정은 탭 수만 대체 | [20260915_settings-window-indicator-tab.md](references/20260915_settings-window-indicator-tab.md) |

### 이벤트 탭 — 진입·번역·수명

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> Superseded (부분) by [20260915_settings-window-indicator-tab.md](20260915_settings-window-indicator-tab.md) — 탭 수(3탭 → General / Indicator / Apps / About 4탭) / 권한 섹션 자리 이동·Diagnostics 접기·460×560 고정·기본 선택 Apps는 유효

# 설정 창은 3탭 — General / Apps / About

- **결정일**: 2026-08-09
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 온스크린 인디케이터 opt-in 확장 — 순간 표시만 옵션 + 포커스 창 테두리 스타일

<!-- 파일명 규칙: yyyymmdd_<kebab-case-title>.md — 날짜는 결정일. 이 문서는 결정의 불변 스냅샷이며, 기록 후 수정하지 않습니다 (Superseded 마킹 1줄 제외). -->

- **결정일**: 2026-09-14

## 결정

온스크린 모드 인디케이터에 **opt-in 옵션 둘**을 추가한다. 기본값은 바꾸지 않는다 (전환 시 순간 표시 + 비-Insert 상시 배지).

1. **상시 표시 on/off** (`onScreenModeIndicatorPersistentEnabled`, 기본 on): off면 모드 전환 때의 순간 표시(flash)만 남고 Normal·Visual 동안 아무것도 남지 않는다. 기존 on/off 토글은 그대로 두고 그 아래 Picker("Only when the mode changes" / "Also while in Normal or Visual mode")로 고른다 — Bool 키 하나를 더하는 additive 방식이라 저장값 마이그레이션이 없다.
2. **상시 표시 스타일에 포커스 창 테두리 추가** (`ModeIndicatorPresentationStyle.windowBorder`): 포커스 창 rect 자체를 강조색 테두리로 두르고 창 안쪽 오른쪽 위에 모드 라벨을 놓는다. 배지 / 창 테두리 / 화면 테두리 셋 중 고른다. 창이 없으면 **폴백 없이** 아무것도 표시하지 않는다.

같이 확정한 것: 자기 pid 경로(설정 창·메뉴바 클릭으로 VimAction이 최전면)는 떠 있는 기하 읽기를 **무효화**하고 밀린 flash를 **만료**한다. 두 옵션 모두 설정 창에서 바꾸므로 이 경로의 정합성이 전제다 (PR #68 리뷰에서 지적된 구멍).

## 배경·근거 (왜)

PR 3 도그푸딩 피드백. [20260906_mode-indicator-hybrid-display-policy.md](20260906_mode-indicator-hybrid-display-policy.md)가 "순간 표시만"과 "창 단위 하이라이트"를 **기본으로는** 기각한 이유는 유지된다 — 순간 표시만으로는 사후 인지("딴 데 갔다 와서 지금 Normal이었나?")를 못 풀고, 창 단위 표시는 어느 입력칸인지 말하지 못한다. 그래서 기본값은 그대로다. 다만:

- 상시 배지가 **소음**으로 느껴지는 사용자에게는 순간 표시만이 더 낫다. 안전 장치를 통째로 끄는 것(토글 off)과 전환 피드백은 남기는 것 사이에 단계가 없었다.
- 창 테두리는 라벨을 동반하므로 "어느 모드인지"는 말한다(색만으로 구분하지 않는다는 NFR 유지). "어느 입력칸인지"를 포기하는 대신 시야 어디서든 보이는 큰 신호를 원하는 사용자의 선택지다 — 화면 테두리보다 덜 압도적이고 배지보다 눈에 띈다.
- 두 옵션은 기존 구조에 거의 공짜다: 순간 표시만은 Insert 경로(`showsBadge == false`)와 같은 코드이고, 창 테두리는 이미 있는 테두리 패널과 창 rect 읽기·창 이동/리사이즈 알림을 재사용한다.
- 자기 pid 토큰: 두 새 설정도 기존 스타일 설정과 같이 설정 창(= 자기 pid 경로)에서 바뀐다. 이 경로가 떠 있는 읽기를 살려 두면 이전 앱을 향해 떠 있던 읽기가 착지해 방금 감춘 옛 형태를 되살린다. 밀린 flash를 만료시키는 것은 "앵커를 못 찾은 flash는 버린다"는 기존 계약과 같은 원칙이다.

## 검토한 대안

- **on/off 토글을 3단 Picker(off / 전환 시만 / 항시)로 교체**: 기각 — 기존 Bool 키를 enum으로 바꾸려면 마이그레이션 코드와 테스트가 따라온다. 토글 + 하위 Picker면 UX가 같고 코드가 절반이다.
- **창 테두리를 창 사다리 폴백으로만 흡수**(09-06 결정의 원안): 유지 — 폴백은 그대로 있고, 이번 것은 사용자가 **고르는** 스타일이다. 둘은 다른 축이다.
- **창 테두리에서 창이 없을 때 요소·화면으로 폴백**: 기각 — "창 테두리"인데 다른 것을 두르면 사용자가 고른 스타일이 아니다. 배지가 앵커 없을 때와 같은 답("없음")이 일관된다.
- **테두리 모서리 반경을 스타일별로 가르기**: 보류 — 선 굵기의 2배(8pt)를 두 테두리가 공유한다. 실기기에서 창 모서리와 어긋나 보이지 않아 갈라야 할 이유가 아직 없다.
- **자기 pid 경로에서 밀린 flash 보존**: 기각 — 보존하면 복귀 시점의 앵커 이벤트에 얹혀 한참 전 전환의 라벨이 뒤늦게 번쩍인다.

## 영향 범위

- 갱신한 architecture reference: [mode-indicator-overlay.md](../../architecture/references/mode-indicator-overlay.md), [profiles-and-config.md](../../architecture/references/profiles-and-config.md)(UserDefaults 키 셋), [app-shell.md](../../architecture/references/app-shell.md)(Behavior 항목)
- 코드: `ModeIndicatorController`(설정 프로퍼티 셋, `borderPanelStyle`, 자기 pid 토큰 무효화, `usesBorderPanel`), `ModeIndicatorGeometryReader.read(processID:includesCaret:includesWindow:)`, `ModeIndicatorLayout.windowBorderLayout`/`screenBorderLayout`(개명), `Preferences.swift`, `SettingsView.swift`.
- Settings General > Behavior의 인디케이터 항목이 셋(토글·"Show" Picker·"Indicator style" Picker)이 됐다. 색상·투명도 후속에서 Visual 탭 분리를 검토할 때 이 셋도 같이 옮긴다 (탭 구성은 [20260809_settings-window-three-tabs.md](20260809_settings-window-three-tabs.md)).
- 09-06 결정 다섯 항목을 뒤집지 않는다 — Supersedes 없음.
Loading