feat(ollama): v0.2.3.1 — in-app endpoint/model 설정 #21

Merged
altair823 merged 13 commits from feat/v0231-ollama-settings into main 2026-05-04 15:00:43 +00:00
Owner

Summary

v0.2.3 dogfood unblock 패치. INKLING_OLLAMA_ENDPOINT env var 의존 제거 — 트레이/배너에서 직접 endpoint + model 변경 가능. Windows 의 dynamic port 점유 (Hyper-V/WSL2 NAT) 같은 환경 이슈에 즉시 대응.

Decisions (mini-brainstorm)

  • Q1=B: Endpoint + Model 둘 다 (LAN 서버 fallback 시 model 도 다를 수 있음)
  • Q2=A: Freetext input (dropdown 은 v0.2.4 영역)
  • Q3=B: JSON file (<profileDir>/settings.json, migration v4 회피)

자명 결정: precedence settings>env>default / save 전 healthCheck / AbortController + provider re-create / 트레이 + 배너 진입점.

Changes

  • SettingsService: JSON 영속화 + zod 검증 + atomic write (temp + rename)
  • ProviderHolder: mutable holder + listeners, indirection layer
  • LocalOllamaProvider: AbortController instance field + abort() public method
  • InferenceProvider: abort?: () => void optional (T3 review 추가)
  • AiWorker / HealthChecker: provider → holder.get() refactor
  • index.ts: settings.load() 후 endpoint/model 결정 (settings > env > default)
  • IPC: inbox:loadOllamaSettings + inbox:saveOllamaSettings + inbox:openOllamaSettings push
  • OllamaSettingsModal (신규): freetext 2 입력 + 저장 시 healthCheck 검증
  • OllamaBanner: "설정" 링크 추가
  • App.tsx: modal mount + IPC subscribe
  • tray.ts: 10번째 callback "Ollama 설정..." 메뉴
  • package.json: 0.2.3 → 0.2.3.1

Spec & Plan

  • Spec: docs/superpowers/specs/2026-05-04-v0231-ollama-settings-design.md
  • Plan: docs/superpowers/plans/2026-05-04-v0231-ollama-settings.md

Test Plan

  • typecheck 0
  • 단위 403 → 413 (+10: SettingsService 6, ProviderHolder 2, LocalOllamaProvider 2)
  • e2e 1/1
  • dogfood: Windows 11434 막힌 환경에서 설정 변경 → 정상 동작
  • dogfood: macOS dogfood 시 settings.json 위치 확인 (~/Library/Application Support/Inkling/Inkling/profiles/default/)
  • dogfood: 잘못된 endpoint 입력 시 inline 에러 표시
  • dogfood: 저장 후 OllamaBanner 즉시 갱신 (60s 대기 X — health.runOnce)

Roadmap

머지 후 v0.2.3.1 binary 재빌드 (Windows + Mac) + Gitea release v0.2.3.1. dogfood soak ≥1주 후 v0.2.4 brainstorm 트리거 시 backlog 38건 + 신규 피드백 + 본 cut의 ollama_settings_changed count-only telemetry 후속 결정 일괄 triage.

Final reviewer 칭찬

  • ProviderHolder = 36줄 깔끔한 indirection
  • atomic write canonical 패턴
  • 7 invariants 모두 실현
  • T3/T4 review fix 이미 landed
## Summary v0.2.3 dogfood unblock 패치. `INKLING_OLLAMA_ENDPOINT` env var 의존 제거 — 트레이/배너에서 직접 endpoint + model 변경 가능. Windows 의 dynamic port 점유 (Hyper-V/WSL2 NAT) 같은 환경 이슈에 즉시 대응. ## Decisions (mini-brainstorm) - Q1=B: Endpoint + Model 둘 다 (LAN 서버 fallback 시 model 도 다를 수 있음) - Q2=A: Freetext input (dropdown 은 v0.2.4 영역) - Q3=B: JSON file (`<profileDir>/settings.json`, migration v4 회피) 자명 결정: precedence settings>env>default / save 전 healthCheck / AbortController + provider re-create / 트레이 + 배너 진입점. ## Changes - **SettingsService**: JSON 영속화 + zod 검증 + atomic write (temp + rename) - **ProviderHolder**: mutable holder + listeners, indirection layer - **LocalOllamaProvider**: AbortController instance field + `abort()` public method - **InferenceProvider**: `abort?: () => void` optional (T3 review 추가) - **AiWorker / HealthChecker**: provider → holder.get() refactor - **index.ts**: settings.load() 후 endpoint/model 결정 (settings > env > default) - **IPC**: `inbox:loadOllamaSettings` + `inbox:saveOllamaSettings` + `inbox:openOllamaSettings` push - **OllamaSettingsModal** (신규): freetext 2 입력 + 저장 시 healthCheck 검증 - **OllamaBanner**: "설정" 링크 추가 - **App.tsx**: modal mount + IPC subscribe - **tray.ts**: 10번째 callback "Ollama 설정..." 메뉴 - **package.json**: 0.2.3 → 0.2.3.1 ## Spec & Plan - Spec: `docs/superpowers/specs/2026-05-04-v0231-ollama-settings-design.md` - Plan: `docs/superpowers/plans/2026-05-04-v0231-ollama-settings.md` ## Test Plan - [x] typecheck 0 - [x] 단위 403 → 413 (+10: SettingsService 6, ProviderHolder 2, LocalOllamaProvider 2) - [x] e2e 1/1 - [ ] dogfood: Windows 11434 막힌 환경에서 설정 변경 → 정상 동작 - [ ] dogfood: macOS dogfood 시 settings.json 위치 확인 (`~/Library/Application Support/Inkling/Inkling/profiles/default/`) - [ ] dogfood: 잘못된 endpoint 입력 시 inline 에러 표시 - [ ] dogfood: 저장 후 OllamaBanner 즉시 갱신 (60s 대기 X — health.runOnce) ## Roadmap 머지 후 v0.2.3.1 binary 재빌드 (Windows + Mac) + Gitea release v0.2.3.1. dogfood soak ≥1주 후 v0.2.4 brainstorm 트리거 시 backlog 38건 + 신규 피드백 + 본 cut의 ollama_settings_changed count-only telemetry 후속 결정 일괄 triage. ## Final reviewer 칭찬 - ProviderHolder = 36줄 깔끔한 indirection - atomic write canonical 패턴 - 7 invariants 모두 실현 - T3/T4 review fix 이미 landed
altair823 added 11 commits 2026-05-04 14:47:57 +00:00
mini-brainstorm 3개 결정:
- Q1=B: Endpoint + Model 둘 다 포함
- Q2=A: Freetext input (dropdown 은 v0.2.4 영역)
- Q3=B: JSON file (`<profileDir>/settings.json`, migration v4 회피)

자명 결정 (질문 없이 패턴):
- precedence: settings > env > default
- in-flight: AbortController abort + provider re-create
- UI: 트레이 + OllamaBanner 진입점, React modal
- validation: save 전 healthCheck

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T1 SettingsService (JSON 영속화 + zod, +6 cases)
T2 LocalOllamaProvider abort + model param (+2 cases)
T3 ProviderHolder + AiWorker/HealthChecker refactor (+2 cases)
T4 index 부팅 + IPC + preload + types
T5 OllamaSettingsModal + App.tsx + OllamaBanner 링크
T6 트레이 메뉴 "Ollama 설정..."
T7 Closure (version 0.2.3 → 0.2.3.1 + gates)

총 신규 단위 +10. 단위 403 → 413.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `<profileDir>/settings.json` atomic write (temp + rename)
- 손상 JSON / 파일 없음 → 빈 객체 fallback (no throw)
- in-memory cache (load 1회 file read)
- zod .strict() schema for ollama { endpoint: URL, model: string }
- 단위 +6 cases

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- abortController 가 method-local 에서 private instance field 로 이동
- public abort() 메서드 — 외부에서 in-flight generate 강제 중단
- ProviderHolder.replace() 시 호출되어 endpoint 변경 즉시 반영
- 단위 +2 cases (abort cancellation, model 파라미터)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ProviderHolder: mutable holder + listeners, indirection layer
- AiWorker: constructor InferenceProvider → ProviderHolder
  this.provider.x → this.holder.get().x 전환
- HealthChecker: 동일 패턴
- src/main/index.ts: provider 를 ProviderHolder 로 감싸서 생성
- 기존 AiWorker / HealthChecker 테스트의 constructor 호출에 ProviderHolder wrap
- 단위 +2 cases (ProviderHolder)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T3 가 ProviderHolder 를 InferenceProvider 로 추상화. 단 IPC handler 가
holder.get().abort() 호출 예정 — interface 에 method 가 없으면 typecheck 실패.

abort 는 in-flight generate 중단용이라 모든 provider 가 지원할 필요는 없음
→ optional method 로 추가. caller 는 holder.get().abort?.() 패턴 사용.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- index.ts: SettingsService.load() 후 endpoint/model 결정 (settings > env > default)
- IPC: inbox:loadOllamaSettings + inbox:saveOllamaSettings
  - save: 임시 provider 로 healthCheck 통과 시에만 영속화 + holder.replace
  - 기존 in-flight generate 는 abort?.() (optional method)
- preload + InboxApi shared types

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T4 fallback comment "60s polling cycle" 대신 HealthChecker 의 기존 public
method runOnce() 사용. 사용자가 settings 저장하자마자 OllamaBanner 갱신.
runOnce 는 이미 inbox:ollamaRecheck IPC 가 사용 중인 패턴.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- OllamaSettingsModal: endpoint + model freetext 입력, 저장 시 healthCheck → 성공 닫기, 실패 inline 에러
- App.tsx: ollamaSettingsOpen state + onOpenOllamaSettings IPC subscribe
- OllamaBanner: onOpenSettings prop 추가, 우측 "설정" 버튼
- preload + types: onOpenOllamaSettings listener bridge

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- createTray 10번째 positional callback runOpenOllamaSettings
- 트레이 → 메뉴 클릭 → main 이 inbox:openOllamaSettings IPC push
- renderer App.tsx 가 구독해 modal open

backlog #4/#26 (TrayCallbacks object refactor) 와 합산 — v0.2.4 시 일괄 정리

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dogfood unblock 패치. v0.2.3 의 INKLING_OLLAMA_ENDPOINT env var 의존 →
in-app UI (트레이 + 배너) 에서 endpoint + model 변경 가능.

게이트: typecheck 0 / 단위 413 / e2e 1
다음: PR + 머지 후 binary 재빌드 + Gitea release v0.2.3.1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
Owner

Round 1 review (controller-side)

항목 결과
Critical 0
Important 1 (i1: save vs HealthChecker tick race — acknowledge only)
Minor 6
Nit 2

i1 (Important) — save vs periodic tick race

saveOllamaSettingshealth.runOnce() 후 응답, 동시에 60s 주기 tick 가능. inFlight 가드로 race 없지만 modal 닫히기 전에 banner flicker 가능. UX 영향 낮고 정확성 영향 0. acknowledge only, fix 안 함.

Inline fixes (m1+m3+m4+n1, 4건)

  • m1: setOllama() throw (disk full / EPERM 등) 시 raw exception 노출 → IPC handler 가 try/catch 후 { ok: false, reason: 'persist failed: ...' } 대칭 응답
  • m3: Modal 에 ESC (close) + Enter (save) 키 핸들러 추가 + first input autofocus
  • m4: handleSave 첫 줄 if (saving) return; — synchronous double-click 가드
  • n1: gemma4:e4b magic constant → DEFAULT_OLLAMA_MODEL 로 hoist (index.ts + modal 공유)

Deferred to v0.2.4 backlog

  • m2 (Minor → backlog): healthCheck reason 에 endpoint URL 노출 → ollama_unreachable.reason telemetry 우회 PII. v0.2.3.1 의 in-app endpoint UI 가 LAN 사용을 흔하게 만들어 더 두드러짐. v0.2.4 telemetry 하드닝 시 reason masking 정책.

Skip 항목

  • m5 (abort() try/catch wrap): 현재 LocalOllamaProvider.abort?.abort() 만 — throw X. defensive 가드 YAGNI. 다른 provider 추가 시 검토.
  • m6 (first-boot blocking await): 10-50ms cold disk read. 무시 가능, 패턴 polluting.
  • n2 (offReplace 누락): 현재 listener 추가하는 callsite 0건 (per-call get() 패턴). 필요 시 추가.

Spec 컴플라이언스

  • Q1=B / Q2=A / Q3=B 모두
  • 7 invariants 모두
  • Privacy invariant ⚠️ partial — m2 가 우회 노출 경로

Verdict

APPROVE WITH FIX — m1+m3+m4+n1 inline, m2 backlog → round 2.

## Round 1 review (controller-side) | 항목 | 결과 | |---|---| | Critical | 0 | | Important | 1 (i1: save vs HealthChecker tick race — acknowledge only) | | Minor | 6 | | Nit | 2 | ### i1 (Important) — save vs periodic tick race `saveOllamaSettings` 가 `health.runOnce()` 후 응답, 동시에 60s 주기 tick 가능. `inFlight` 가드로 race 없지만 modal 닫히기 전에 banner flicker 가능. UX 영향 낮고 정확성 영향 0. **acknowledge only**, fix 안 함. ### Inline fixes (m1+m3+m4+n1, 4건) - **m1**: `setOllama()` throw (disk full / EPERM 등) 시 raw exception 노출 → IPC handler 가 try/catch 후 `{ ok: false, reason: 'persist failed: ...' }` 대칭 응답 - **m3**: Modal 에 ESC (close) + Enter (save) 키 핸들러 추가 + first input autofocus - **m4**: `handleSave` 첫 줄 `if (saving) return;` — synchronous double-click 가드 - **n1**: `gemma4:e4b` magic constant → `DEFAULT_OLLAMA_MODEL` 로 hoist (index.ts + modal 공유) ### Deferred to v0.2.4 backlog - **m2** (Minor → backlog): healthCheck reason 에 endpoint URL 노출 → `ollama_unreachable.reason` telemetry 우회 PII. v0.2.3.1 의 in-app endpoint UI 가 LAN 사용을 흔하게 만들어 더 두드러짐. v0.2.4 telemetry 하드닝 시 reason masking 정책. ### Skip 항목 - **m5** (abort() try/catch wrap): 현재 `LocalOllamaProvider.abort` 는 `?.abort()` 만 — throw X. defensive 가드 YAGNI. 다른 provider 추가 시 검토. - **m6** (first-boot blocking await): 10-50ms cold disk read. 무시 가능, 패턴 polluting. - **n2** (offReplace 누락): 현재 listener 추가하는 callsite 0건 (per-call get() 패턴). 필요 시 추가. ### Spec 컴플라이언스 - Q1=B / Q2=A / Q3=B 모두 ✅ - 7 invariants 모두 ✅ - Privacy invariant ⚠️ partial — m2 가 우회 노출 경로 ### Verdict **APPROVE WITH FIX** — m1+m3+m4+n1 inline, m2 backlog → round 2.
altair823 added 1 commit 2026-05-04 14:54:03 +00:00
- m1 (Minor): saveOllamaSettings IPC가 setOllama throw 시 try/catch
  → { ok: false, reason: 'persist failed: ...' } 대칭 응답
- m3 (Minor): Modal ESC=close + Enter=save 키 핸들러 + 첫 input autoFocus
- m4 (Minor): handleSave 첫 줄 if (saving) return; — sync double-click 가드
- n1 (Nit): 'gemma4:e4b' / 'http://localhost:11434' magic
  → src/shared/constants.ts 의 DEFAULT_OLLAMA_MODEL / DEFAULT_OLLAMA_ENDPOINT

defer to v0.2.4 backlog:
- m2: ollama_unreachable.reason 에 endpoint URL 노출 (PII 우회) — telemetry masking 정책

skip:
- i1 (race UX): acknowledge only, 정확성 영향 0
- m5 (abort try/catch): 현재 LocalOllamaProvider.abort 는 throw X
- m6 (first-boot blocking): 무시 가능
- n2 (offReplace): 현재 listener callsite 0건

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
altair823 added 1 commit 2026-05-04 14:55:01 +00:00
PR #21 round 1 review 에서 deferred 항목들 backlog 38 → 43:
- #39 (m2): ollama_unreachable.reason 의 endpoint URL PII 우회 노출
- #40 (i1): save vs HealthChecker tick race UX flicker
- #41: OllamaSettingsModal 인라인 스타일 (#24 와 합산)
- #42: Modal client-side URL validation 부재
- #43: createTray 10번째 positional callback (#4/#26 blocker)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
Owner

Round 2 — APPROVE

항목 결과
Round 1 fix commit 6f95e89
Backlog defer commit d974335 (38 → 43, +5건)
m1 (setOllama throw 대칭 응답)
m3 (ESC/Enter + autoFocus)
m4 (handleSave double-click 가드)
n1 (gemma4:e4b → DEFAULT_OLLAMA_MODEL 상수)
m2 (PII reason masking) → backlog #39
i1 (save vs tick race UX flicker) → backlog #40
Round 2 verdict APPROVE (0 new issue)
Gates typecheck 0 / 단위 413 / e2e 1

머지 후 알려줘. closure 단계 (local main sync + 브랜치 정리 + memory backlog status 업데이트 + binary 빌드) 진행.

## Round 2 — APPROVE | 항목 | 결과 | |---|---| | Round 1 fix commit | `6f95e89` | | Backlog defer commit | `d974335` (38 → 43, +5건) | | m1 (setOllama throw 대칭 응답) | ✅ | | m3 (ESC/Enter + autoFocus) | ✅ | | m4 (handleSave double-click 가드) | ✅ | | n1 (gemma4:e4b → DEFAULT_OLLAMA_MODEL 상수) | ✅ | | m2 (PII reason masking) | → backlog #39 | | i1 (save vs tick race UX flicker) | → backlog #40 | | Round 2 verdict | **APPROVE** (0 new issue) | | Gates | typecheck 0 / 단위 413 / e2e 1 | 머지 후 알려줘. closure 단계 (local main sync + 브랜치 정리 + memory backlog status 업데이트 + binary 빌드) 진행.
altair823 merged commit fee982a6e6 into main 2026-05-04 15:00:43 +00:00
altair823 deleted branch feat/v0231-ollama-settings 2026-05-04 15:00:44 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/inkling#21