release: ship v1.5.0 with on-device writing suggestions
Some checks failed
deploy-site / deploy (push) Failing after 33s
portable-unsigned / portable-windows (push) Failing after 4m7s
release / release-windows (push) Failing after 3m16s

Adds next-sentence suggestions while typing, weekly input insights and a
personal phrase memory to the desktop app, and fixes custom instructions so
they process the text instead of inserting the instruction's own wording.
Local model requests are now bounded and individually cancellable.

Bumps the product version to 1.5.0 (Android/iOS build 1050000), refreshes the
landing and web download links, and records the new INPUT feature rows and the
open verification gaps in the infrastructure map.
This commit is contained in:
Yun Chan 2026-09-23 16:04:27 +09:00
parent 99f06c253c
commit 5c11ee2fde
104 changed files with 14410 additions and 174 deletions

View file

@ -72,6 +72,16 @@ Legend: `[ ]` open · `[~]` in progress · `[!]` blocked externally · `[x]` res
| GAP-INFRA-05 | Build | 패키징된 렌더러 팝업 스크립트가 번들에 없었다. 팝업 HTML이 classic `<script src="./script.js">`를 참조해 Vite가 처리하지 않았고, dev에서는 로드되지만 설치본에는 파일이 없었다. 그래서 녹음 오버레이가 0:00에서 멈추고 웨이브 바가 뜨지 않았으며 실시간 자막이 렌더되지 않았다. 로드 전 `webContents.send`가 조용히 버려지는 문제와 `hide()` 이후 재표시의 z-order/repaint 유실도 함께 있었다. | `apps/desktop/src/renderer/popups/*/index.html`, `apps/desktop/src/main/windows/WindowManager.ts`, `scripts/ci/verify-desktop-renderer-bundles.mjs` | `[x]` 2026-09-19: 팝업 5종을 `type="module"`로 전환해 Vite가 해시된 번들로 방출하도록 고쳤고, 빌드 HTML이 참조하는 모든 로컬 asset이 디스크에 있는지 검사하는 `verify-desktop-renderer-bundles.mjs`(+ self-test)를 `.forgejo`/`.github` 패키징 파이프라인에 연결했다. WindowManager는 렌더러 준비 전 IPC를 `did-finish-load`까지 보관하고, 팝업을 표시할 때마다 topmost 재선언 + 강제 repaint를 수행하며, 팝업 렌더러 콘솔/로드 실패를 main 로그로 승격한다. |
| GAP-INPUT-01 | Input intelligence | **2026-09-22 10:57 실사용 검증 성공** — 카톡 타이핑 중 제안 5개 생성(1436ms)이 로그로 확인됐고(조합 중·idle 25ms에서도), 50초 관찰에 스냅샷 67건/생성 3회/실패 0건이었다. 남은 확인: 오버레이 위치·외관, 수락 삽입, 비밀번호 차단, 주간 수치 48개 유닛 케이스는 정책/집계/프롬프트 같은 순수 함수만 덮는다. 검증되지 않은 것: (1) `Alt+Shift+←` 수락이 실제 앱에 문장을 삽입하는지, (2) 오버레이가 케어렛 옆에 붙는지(케어렛 rect Chromium 에서만 UIA 로 얻어지고 네이티브 Win32 앱은 `GetGUIThreadInfo` 경로가 아예 없다), (3) `TypeError` 없이 Electron 메인에서 `koffi` 가 로드되는지(현재 호스트 Node 에서만 확인), (4) PyInstaller 번들에 `uiautomation`/`comtypes` 가 실제로 들어가는지(`build-sidecar.mjs` 에 `--collect-all` 을 추가했지만 번들을 다시 빌드해 확인하지 않았다). 에이전트는 데스크톱 GUI 를 띄울 수 없다(`AGENTS.md` §3). | `apps/desktop/src/main/services/{InputTelemetryService,SuggestionService,UiaContextService}.ts`, `apps/desktop/src/main/utils/win32-foreground.ts`, `apps/desktop/sidecar/{uia_bridge.py,requirements.txt}`, `apps/desktop/scripts/build-sidecar.mjs`, `apps/desktop/tests/main/services/input-intelligence.test.ts`(45) + `llm-prompts.test.ts`(+4) | **2026-09-21 23:13 부분 검증 완료** (사용자가 실제로 Notepad 에 타이핑한 로그): 텔레메트리 기동, uiohook 후킹, UIA 스냅샷(Notepad — `edit=true pw=false comp=false src=value`), WindowsTerminal 의 문서형 컨트롤을 `not-editable` 로 정확히 거부, 판단 로그까지 전부 실동작 확인. 이 과정에서 결함 4건을 잡아 고쳤다: (a) 트리거 지연(1000ms)이 스냅샷 디바운스(700ms)보다 커서 **"멈춘 뒤" 게이트가 결코 열리지 않던 문제**(settle 패스 추가), (b) 켜 둔 상태로 앱을 켜면 워밍업이 한 번도 안 돌던 문제(부팅 시 워밍업 + 가용성 폴링 대기 재시도), (c) 케어렛 오프셋을 못 주는 앱에서 문서 전체를 접두로 쓰던 문제(tail 폴백), (d) 후보가 도착해야 오버레이가 떠서 "아무것도 안 나옴" 으로 보이던 문제(요청 즉시 "생성 중" 표시). **2026-09-22 09:00~09:17 추가 실측** (사용자가 KakaoTalk·WindowsTerminal·Agent Switchboard 에 타이핑한 17개 스냅샷): 편집 가능으로 판정된 것은 3건뿐이고, 그중 KakaoTalk 입력창은 `edit=true src=value` 인데도 `len=0` 이라 실제 내용을 못 읽었다 — 커스텀 렌더 앱(카톡·터미널·에이전트 UI)에서는 UIA 가 텍스트를 노출하지 않는다. 즉 "아무것도 안 나옴" 의 상당 부분은 결함이 아니라 **읽을 수 없는 앱에서의 정상 동작**이며, 이 구분이 사용자에게 보이지 않던 것이 문제였다. 그래서 설정 > 입력에 **실시간 진단 줄**(포커스 앱 · 읽기 가부 · 소스 · 글자 수 · 비밀번호/케어렛 폴백 표시)을 추가하고 12개 로케일에 문구를 넣었다. 남은 확인: 오버레이의 실제 위치·외관, `Alt+Shift+←` 수락 삽입, 비밀번호 필드 차단, 주간 수치 정확도. 함께: `sidecar:build` 후 `GET /uia/focus` 응답을 확인하고 그 결과를 `scripts/ci/verify-sidecar-bundle.mjs` 의 필수 항목에 반영한다. **검증 도구 주의**: 데스크톱 `npm run typecheck` 는 문서화된 대로 no-op 이라(GAP-INFRA-04) 이번 작업에서도 거짓 통과를 냈다 — `typecheck:strict` 로 다시 돌려 이 작업이 만든 타입 오류 7건(core 에 없는 `InputTelemetryState` 참조 5건 등)을 찾아 고쳤다. 현재 strict 기준 이 작업 파일들의 오류는 0건이다(main 13 / renderer 35 는 전부 선재). |
| GAP-INPUT-07 | Input intelligence | `[~]` **2026-09-23 focused automated evidence:** five test files / 80 tests passed; desktop typecheck/lint, Python `py_compile`, and `git diff --check` exited 0 (the core 131-test pass is earlier independent evidence). The overlay lifecycle and `windowsHide:true` child-process audit are code/automation evidence only, not Electron GUI or external-app insertion proof. | `InputTelemetryService.ts`, `SuggestionService.ts`, `KeyBindingService.ts`, `components/input-insights/{InputConsentPanel,InputInsightsView}.tsx`, `popups/suggestion-overlay/`, `tests/main/services/{input-flow-domain,input-flow-services,windows-child-process-hide}.test.ts` | From an external terminal restart with `run-desktop.bat`, verify: (1) click X during generation closes immediately and never reappears; (2) selecting text closes the overlay; (3) input-focus loss closes it; (4) TTS, voice action, audio enumeration, screen context and file transcription do not revive a cmd/PowerShell window. Retain the existing editable/read-unavailable/password, Flow Radar/Edit Friction/App Quality, receipt, Smart Exclusion, provenance/fallback, shortcut-audit and narrow-geometry checks. Automated tests do **not** replace this manual proof. |
| GAP-INPUT-02 | Input intelligence | **IME 조합 중 텍스트(preedit)는 수집되지 않는다.** 우리는 조합 중에는 통계·제안을 모두 억제하고 조합이 끝난 커밋 텍스트만 UIA 로 읽는다. 조합 문자열 자체를 읽으려면 `IUIAutomationTextEditPattern::GetActiveComposition`(또는 레거시 앱은 IME `ImmGetCompositionStringW`)이 필요하다. KeyType.Windows 도 이 부분을 명시적으로 다음 사이클로 미룬다 — 즉 업계 공통 미해결 지점이다. 조합 중 억제 자체는 MS Learn 의 IME 문서상 필수 조치이며 구현돼 있다. | `apps/desktop/sidecar/uia_bridge.py`(`_caret_rect_and_offset` 의 TextEditPattern 블록), `packages/core/src/input-intelligence.ts`(`decideSuggestion` 의 `composing` 분기) | 조합 범위를 읽어 "조합 중 미리보기"를 제안 후보로 쓸지 검토한다. 지금은 억제만 하고 있어 한국어 사용자는 조합을 끝내야 제안이 뜬다. |
| GAP-INPUT-03 | Input intelligence | **공백이 없는 언어(중국어)는 단어 수가 과소 집계된다.** `countWords` 가 공백 분리 토큰을 세므로 "今天开会讨论了三件事" 는 1단어로 계산된다. 한국어/영어/일본어(공백 사용)는 정상이다. | `packages/core/src/input-intelligence.ts`(`countWords`), 카탈로그 INPUT-03, 테스트 `input-intelligence.test.ts` | CJK 연속 구간을 문자 단위로 세는 분기를 추가하거나, 통계 라벨을 "단어" 대신 "어절"로 바꾼다. |
| GAP-INPUT-04 | Input intelligence | **UIA 브리지가 Windows 전용이다.** `uiautomation` 은 Windows UI Automation 래퍼이므로 macOS 는 AX API, Linux 는 AT-SPI 구현이 따로 필요하다. 그래서 입력 인텔리전스는 카탈로그에서 데스크톱(`D`)만 표기하고 나머지 표면은 `[-]` 다. 또한 Chromium 138 미만 앱은 접근성 트리가 켜져 있으면 입력창 텍스트를 노출하지 않는데, 그 앱들을 `--force-renderer-accessibility` 로 켜도록 강제할 방법이 없다(우리 자신은 `app.setAccessibilitySupportEnabled(true)` 로 처리했다 — `bootstrap.ts`). | `apps/desktop/sidecar/uia_bridge.py`, `apps/desktop/src/main/bootstrap.ts`(`initInputIntelligence`) | macOS AX 경로를 붙일지 결정한다. 붙이지 않으면 카탈로그에서 macOS 를 명시적 N/A 로 유지한다. |
| GAP-INPUT-05 | Input intelligence | **수락/닫기 키가 사용자 습관과 충돌할 수 있다.** 기본값을 `Alt+Shift+←/↓/↑` 로 둔 이유는 Tab·Escape·Ctrl+Space 같은 관례 키를 뺏지 않기 위해서다(우리는 키를 삼키지 않으므로 원래 동작이 함께 실행된다). 그 대가는 "Tab 으로 수락" 같은 자연스러운 조작이 아니라는 점이고, 인라인 컴플리션 도구 대부분(Tab)과 다르다. | `packages/core/src/keybinding.ts`(`suggestion-accept`/`next`/`dismiss`), `apps/desktop/src/main/bootstrap.ts`(트리거 구독) | 오버레이가 보일 때만 Tab 을 삼키는 경로(전역 후킹에서 조건부 suppress)를 검토한다. 지금은 불가능하지 않지만 포커스 없는 창에서 키를 가로채는 설계가 필요하다. |
| GAP-LLM-03 | LLM | `[x]` **2026-09-22 코드 구현 완료.** 공유 `_abortController`를 요청별 controller로 대체하고 외부 signal을 각 요청에 연결/정리한다. generate/stream은 2048 tokens / 120 s, chat은 512 / 60 s로 제한하며, stream은 `done` frame 없이는 성공 처리하지 않고 불완전 응답을 정리한다. VoiceConversation은 single-flight와 request signal을 통해 local cancel을 실제 chat 취소로 전달한다. | `apps/desktop/src/main/services/LocalLLMService.ts`, `VoiceConversationService.ts` | 독립 표적 검증: 6 test files / 69 tests passed / 0 failed. 변경 코드·테스트 ESLint exit 0, `git diff --check` exit 0. 이는 전체 strict typecheck 또는 GUI 검증이 아니다. 앱 재시작 뒤 동시 제안·음성 요청의 요청별 취소와 `done` 누락 실패 처리는 GAP-LLM-04에서 계속 확인한다. |
| GAP-LLM-04 | LLM | `[~]` **Gemma/Ollama residency·burst·unbounded chat root cause (2026-09-22).** 19:14:12 boot warmup의 `keep_alive: 30m`가 `gemma4:e4b`를 expiry 19:44:12까지 VRAM 3,226,342,521 bytes / context 4096으로 상주시켰다. Windows GPU Engine PID 표본에는 활성 Ollama compute가 없어 무한 추론이 아니라 강제 residency였다. 19:11:17–19:11:58 자동 제안 반복은 기존 900 ms / 12 per min / 5 candidates / 128 tokens / 1-char growth가 허용한 burst였다. 위험 경로는 chat의 무제한 `num_predict`(Ollama 기본 `-1`), timeout·외부 abort 부재, 공유 취소, `done` 없는 EOF 성공, voice cancel 미전파, watchdog의 비취소였다. | `apps/desktop/src/main/services/{LocalLLMService,SuggestionService,VoiceConversationService}.ts`, Ollama `/api/ps` and Windows GPU Engine observations | 구현 계약: boot warmup 제거, suggestion `keep_alive: 2m`; 600 ms debounce / 5 s interval / 6 per min (hard max 12) / 3 candidates / 64 tokens / 12-char growth / 8 s timeout; 요청별 cancellation, bounded requests, done/cleanup, voice single-flight, spawn/poll dedupe와 dispose. Raw Ollama 증거: cold bounded 요청은 client hard timeout 15.044 s에 취소된 뒤 `/api/ps` empty와 `/api/version` 80 ms 회복을 보였고, explicit warmup HTTP 200은 16.639 s, 후속 `num_predict=1` / `keep_alive='2m'` 요청은 553 ms HTTP 200 / `done:true` / `eval_count:1` / `response=OK` / `done_reason:length`였으며 `/api/ps` expiry는 약 119.9 s였다. 이어 19:48:59 +09:00에는 새 generate/unload/kill/retry 없이 충분히 지난 뒤 단일 `/api/ps`가 HTTP 200 / 45.8 ms / `{models:[]}`였고 `/api/version`은 HTTP 200 / 7.3 ms / `0.32.13`이었다. 이는 raw API 수준의 expiry 뒤 unload 확인이다. **남은 조건:** 앱 재시작·GUI·실제 자동제안 타이핑 증거는 아니므로 그 경로에서 rate limit, timeout/cancel, 2분 residency를 확인한다. |
| GAP-INPUT-06 | Input intelligence | `[~]` **제안 폭주 상한 및 runtime 확인.** 이전 24.7 s / 4.9 s, boot warmup, `keep_alive: 30m`, 48/96/128 tokens 및 2/5 candidates 기록은 역사적 가설/완화 이력이며 현재 정책이 아니다. 현 정책은 600 ms debounce, 5 s minimum interval, 6 requests/min default (hard max 12), 3 candidates, 64 output tokens, 12-character growth, 8 s timeout, `keep_alive: 2m` 및 boot warmup 제거다. | `apps/desktop/src/main/services/SuggestionService.ts`, `packages/core/src/input-intelligence.ts`, `LocalLLMService.ts` | 표적 자동 검증은 6 files / 69 passed / 0 failed이며 raw Ollama의 15.044 s cold timeout cleanup, 약 119.9 s residency expiry, 그리고 19:48:59 +09:00에 새 generate/unload/kill/retry 없이 확인한 `/api/ps` HTTP 200 / 45.8 ms / `{models:[]}` (`/api/version` HTTP 200 / 7.3 ms / `0.32.13`)도 확인됐다. 전체 strict typecheck는 green이 아니며, 이 raw API unload 증거는 앱 재시작·GUI·실제 자동제안 타이핑 검증이 아니다. 그 경로에서 rate limit, 8 s abort, 3-candidate/64-token 상한을 확인한다. |
---
## 2. Mobile checklist roll-up (from `MOBILE_APP_COMPLETION_SSOT.md` §4)
@ -188,4 +198,5 @@ Actionable checklist for the work started this session. Fields to fill are blank
| ID | 제약 | 왜 이대로 두는가 | 완화 장치 |
|---|---|---|---|
| CONSTRAINT-INPUT-01 | 입력 레메트리는 **키 내용을 저장하지 않는다.** 카운터(키 입력·클릭·스크롤)와 마우스 이동 거리, 그리고 **사용자가 명시적으로 동의한 경우에만** 읽은 텍스트(학습 DB)만 남긴다. 키코드→문자 복원은 시도하지 않는다. | ActivityWatch `aw-watcher-input` 이 같은 이유로 같은 선택을 한다 — 전역 키 내용 수집은 보안·프라이버시 파급이 크고, 신뢰를 잃으면 기능 자체가 사라진다(README 원문: "This does not track which keys you press … This is not a keylogger, and never will be"). 게다가 키코드 복원은 한/일 IME 에서 원리적으로 불가능하다(조합 결과가 텍스트다). | 수집은 옵트인(`inputTelemetryEnabled`/`inputLearnTypedText` 기본 false), 비밀번호 필드는 `IsPassword` 로 fail-closed 차단, 앱별 제외, 30일 보존 후 자동 삭제, "수집된 데이터 삭제" 버튼(동의 철회 시 즉시 삭제 + 학습 플래그 해제), 전송 없음(로컬 SQLite 전용). |
| CONSTRAINT-I18N-01 | `packages/core/src/keybinding.ts` 는 i18n 키를 평범한 `string` 으로 노출한다. 렌더러가 `asTranslationKey()`(`apps/desktop/src/renderer/components/keybinding/translation-key.ts:7`)로 경계에서 캐스팅하므로, 존재하지 않는 키를 넘겨도 컴파일러가 잡지 못한다. | core 가 로케일 패키지에 의존하지 않게 하려는 의도적 설계다. 검토한 대안 둘 다 성립하지 않는다 — (A) 키 필드를 리터럴 유니온으로 좁히는 방식은 `KEY_CATALOG` 가 `letterEntries()` 같은 함수 생성부를 포함해 불가능하고, (B) core 가 `@d3ro/i18n` 의 타입 가드를 쓰는 방식은 의존 방향을 core → i18n 으로 역전시켜 `03-shared-packages.md` §6 의 전제를 깬다. 2026-09-21 결정: 현행 유지. | `packages/core/__tests__/keybinding-i18n.test.ts` (14 케이스). core 가 참조하는 키가 12개 로케일 전부에 있는지, 값이 빈 문자열이 아닌지, core 가 렌더러 전용 `keybinding.ui.*` 를 참조하지 않는지 검사한다. 거부 사유 키는 하드코딩 목록이 아니라 실제 `validateBinding` 경로를 태워 수집하므로 새 사유가 생기면 자동으로 커버된다. |