fix(ollama): PR #21 review round 1 — m1+m3+m4+n1 (v0.2.3.1)

- 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>
This commit is contained in:
altair823
2026-05-04 23:53:42 +09:00
parent 3a2ff1a35c
commit 6f95e89456
5 changed files with 29 additions and 11 deletions

View File

@@ -2,6 +2,7 @@ import { request } from 'undici';
import { parseAiResponse, type AiResponse } from './schema.js';
import { buildPrompt } from './prompt.js';
import type { GenerateInput, HealthResult, InferenceProvider } from './InferenceProvider.js';
import { DEFAULT_OLLAMA_ENDPOINT, DEFAULT_OLLAMA_MODEL } from '../../shared/constants.js';
export interface LocalOllamaOptions {
endpoint?: string;
@@ -21,8 +22,8 @@ export class LocalOllamaProvider implements InferenceProvider {
private abortController: AbortController | null = null;
constructor(opts: LocalOllamaOptions = {}) {
this.endpoint = opts.endpoint ?? 'http://localhost:11434';
this.model = opts.model ?? 'gemma4:e4b';
this.endpoint = opts.endpoint ?? DEFAULT_OLLAMA_ENDPOINT;
this.model = opts.model ?? DEFAULT_OLLAMA_MODEL;
this.timeoutMs = opts.timeoutMs ?? 120_000;
this.temperature = opts.temperature ?? 0.2;
this.numPredict = opts.numPredict ?? 512;

View File

@@ -31,6 +31,7 @@ import { ImportService } from './services/ImportService.js';
import { SyncService } from './services/SyncService.js';
import { TelemetryService } from './services/TelemetryService.js';
import { SettingsService } from './services/SettingsService.js';
import { DEFAULT_OLLAMA_ENDPOINT, DEFAULT_OLLAMA_MODEL } from '../shared/constants.js';
const HIDDEN_ARG = '--hidden';
const startedHidden = process.argv.includes(HIDDEN_ARG);
@@ -70,8 +71,8 @@ app.whenReady().then(async () => {
const resolvedEndpoint = settings.ollama?.endpoint
?? process.env.INKLING_OLLAMA_ENDPOINT
?? 'http://localhost:11434';
const resolvedModel = settings.ollama?.model ?? 'gemma4:e4b';
?? DEFAULT_OLLAMA_ENDPOINT;
const resolvedModel = settings.ollama?.model ?? DEFAULT_OLLAMA_MODEL;
logger.info('ai.endpoint', {
endpoint: resolvedEndpoint,

View File

@@ -158,7 +158,11 @@ export function registerInboxApi(deps: InboxIpcDeps): void {
const trial = new LocalOllamaProvider({ endpoint: value.endpoint, model: value.model });
const r = await trial.healthCheck();
if (!r.ok) return { ok: false, reason: r.reason ?? 'unknown' };
await deps.settings.setOllama(value);
try {
await deps.settings.setOllama(value);
} catch (e) {
return { ok: false, reason: `persist failed: ${(e as Error).message}` };
}
deps.providerHolder.get().abort?.();
deps.providerHolder.replace(trial);
// 즉시 health 재확인 → onUpdate callback 통해 OllamaBanner 자동 갱신

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { inboxApi } from '../api.js';
import { DEFAULT_OLLAMA_ENDPOINT, DEFAULT_OLLAMA_MODEL } from '../../../shared/constants.js';
interface Props {
open: boolean;
@@ -7,8 +8,8 @@ interface Props {
}
export function OllamaSettingsModal({ open, onClose }: Props): React.ReactElement | null {
const [endpoint, setEndpoint] = useState('http://localhost:11434');
const [model, setModel] = useState('gemma4:e4b');
const [endpoint, setEndpoint] = useState(DEFAULT_OLLAMA_ENDPOINT);
const [model, setModel] = useState(DEFAULT_OLLAMA_MODEL);
const [error, setError] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
@@ -27,6 +28,7 @@ export function OllamaSettingsModal({ open, onClose }: Props): React.ReactElemen
if (!open) return null;
async function handleSave() {
if (saving) return; // m4 fix: synchronous double-click 가드
setSaving(true);
setError(null);
try {
@@ -44,10 +46,17 @@ export function OllamaSettingsModal({ open, onClose }: Props): React.ReactElemen
}
return (
<div style={{
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)',
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000
}}>
<div
onKeyDown={(e) => {
if (e.key === 'Escape' && !saving) onClose();
if (e.key === 'Enter' && !saving) void handleSave();
}}
tabIndex={-1}
style={{
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)',
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000
}}
>
<div style={{
background: '#fff', borderRadius: 8, padding: 20, minWidth: 400, maxWidth: 500,
boxShadow: '0 4px 16px rgba(0,0,0,0.2)'
@@ -62,6 +71,7 @@ export function OllamaSettingsModal({ open, onClose }: Props): React.ReactElemen
value={endpoint}
onChange={(e) => setEndpoint(e.target.value)}
placeholder="http://localhost:11434"
autoFocus
style={{ width: '100%', padding: '6px 8px', fontSize: 13, border: '1px solid #ccc', borderRadius: 4 }}
disabled={saving}
/>

2
src/shared/constants.ts Normal file
View File

@@ -0,0 +1,2 @@
export const DEFAULT_OLLAMA_MODEL = 'gemma4:e4b';
export const DEFAULT_OLLAMA_ENDPOINT = 'http://localhost:11434';