diff --git a/apps/desktop/src/main/services/ConfigService.ts b/apps/desktop/src/main/services/ConfigService.ts index eac6561..d1b0538 100644 --- a/apps/desktop/src/main/services/ConfigService.ts +++ b/apps/desktop/src/main/services/ConfigService.ts @@ -18,7 +18,7 @@ import { getLogger } from './LoggerService' const logger = getLogger('ConfigService') /** 제안/텔레메트리 튜닝 기본값의 현재 개정판. 기본값을 바꾸면 올린다. */ -const SUGGESTION_TUNING_REVISION = 5 +const SUGGESTION_TUNING_REVISION = 6 const INITIAL_SUGGESTION_TUNING = { suggestionTriggerDelayMs: 300, @@ -59,6 +59,11 @@ function migrateSuggestionTuning(activeStore: ElectronStore): void { } if (current < 5) migrateSuggestionOverlayBindings(activeStore) + // 설정 화면에 없는 값인데 설치본에 100ms 가 남아 치는 도중 생성이 시작됐다 — 하한 미만이면 기본값으로. + if (current < 6) { + const delay = Number(raw.suggestionTriggerDelayMs) + if (!Number.isFinite(delay) || delay < 500) activeStore.set('suggestionTriggerDelayMs', 600) + } activeStore.set('suggestionTuningRevision', SUGGESTION_TUNING_REVISION) logger.info( diff --git a/apps/desktop/src/main/services/SuggestionService.ts b/apps/desktop/src/main/services/SuggestionService.ts index e9b25d5..2410323 100644 --- a/apps/desktop/src/main/services/SuggestionService.ts +++ b/apps/desktop/src/main/services/SuggestionService.ts @@ -356,7 +356,11 @@ class SuggestionService extends EventEmitter { // 즉시 세션을 끝낸다(사용자 요청 사양: 계속 자라는 접두를 따라가지 않는다). // 다음 멈춤에서 정책 게이트를 다시 거쳐 새 세션이 시작된다. if (this.isVisible && !matchesSessionPrefix(this._generatedForPrefix, context.prefix)) { + logger.debug('제안 닫음: 이어서 입력함 — 멈추면 새 문맥으로 다시 만든다') this.dismiss('stale') + // 사용자가 고르지 않고 계속 쳤다 — 이 세션 때문에 최소 간격(5초)에 걸려 + // 정작 멈췄을 때 아무것도 안 뜨면 안 된다. 분당·일일 상한은 그대로 둔다. + this._lastRequestAt = Math.min(this._lastRequestAt, now - SUGGESTION_DEFAULTS.minIntervalMs) this.emit('state-changed', this.getState()) return } @@ -1330,7 +1334,10 @@ class SuggestionService extends EventEmitter { excludedApps: string[] } { return { - triggerDelayMs: configGet('suggestionTriggerDelayMs') || SUGGESTION_DEFAULTS.triggerDelayMs, + triggerDelayMs: Math.max( + SUGGESTION_DEFAULTS.minTriggerDelayMs, + configGet('suggestionTriggerDelayMs') || SUGGESTION_DEFAULTS.triggerDelayMs + ), minPrefixChars: configGet('suggestionMinPrefixChars') || SUGGESTION_DEFAULTS.minPrefixChars, maxRequestsPerMinute: Math.min( diff --git a/apps/desktop/tests/main/services/ConfigService.test.ts b/apps/desktop/tests/main/services/ConfigService.test.ts index 03e839e..40702c7 100644 --- a/apps/desktop/tests/main/services/ConfigService.test.ts +++ b/apps/desktop/tests/main/services/ConfigService.test.ts @@ -48,7 +48,7 @@ describe('ConfigService suggestion tuning migration', () => { ) await initConfigService() - expect(configGet('suggestionTuningRevision')).toBe(5) + expect(configGet('suggestionTuningRevision')).toBe(6) expect(configGet('suggestionTriggerDelayMs')).toBe(600) expect(configGet('suggestionMaxRequestsPerMinute')).toBe(6) expect(configGet('suggestionMinPrefixChars')).toBe(17) @@ -114,7 +114,7 @@ describe('ConfigService suggestion tuning migration', () => { // 옮기지 않는 액션은 그대로 남는다. expect(bindings['suggestion-next']).toEqual(OLD_NEXT) expect(bindings['suggestion-prev']).toEqual(OLD_PREV) - expect(configGet('suggestionTuningRevision')).toBe(5) + expect(configGet('suggestionTuningRevision')).toBe(6) resetInMemoryConfig() }) @@ -144,4 +144,43 @@ describe('ConfigService suggestion tuning migration', () => { resetInMemoryConfig() }) + it('revision 6: 하한(500ms) 미만으로 남은 멈춤 판단 시간만 기본값 600ms로 되돌린다', async () => { + for (const [stored, expected] of [ + [100, 600], + [800, 800], + ] as const) { + const persisted = { suggestionTuningRevision: 5, suggestionTriggerDelayMs: stored } + + class TestStore> { + store: T + + constructor(options: { defaults: T }) { + this.store = { ...options.defaults, ...persisted } as T + } + + get(key: K): T[K] { + return this.store[key] + } + + set(key: K, value: T[K]): void { + this.store[key] = value + } + + delete(key: string): void { + delete this.store[key as keyof T] + } + } + + vi.doMock('electron-store', () => ({ default: TestStore })) + const { configGet, initConfigService, resetInMemoryConfig } = await import( + '../../../src/main/services/ConfigService' + ) + await initConfigService() + expect(configGet('suggestionTuningRevision')).toBe(6) + expect(configGet('suggestionTriggerDelayMs')).toBe(expected) + resetInMemoryConfig() + vi.doUnmock('electron-store') + vi.resetModules() + } + }) }) diff --git a/apps/desktop/tests/main/services/input-flow-services.test.ts b/apps/desktop/tests/main/services/input-flow-services.test.ts index 8f73d3e..4626bc2 100644 --- a/apps/desktop/tests/main/services/input-flow-services.test.ts +++ b/apps/desktop/tests/main/services/input-flow-services.test.ts @@ -477,7 +477,8 @@ describe('입력 플로우 서비스', () => { available: true, appName: 'Notion.exe', windowTitle: '회의록', - idleMs: 300, + // 멈춤 판단 하한(500ms)을 넘긴 멈춤 + idleMs: 700, capturedAt: now, editedSinceFocus: true, typedRecently: true diff --git a/packages/core/src/input-intelligence.ts b/packages/core/src/input-intelligence.ts index 2f625b8..4187c00 100644 --- a/packages/core/src/input-intelligence.ts +++ b/packages/core/src/input-intelligence.ts @@ -1276,6 +1276,12 @@ export const SUGGESTION_DEFAULTS = { */ minPrefixChars: 8, minIntervalMs: 5000, + /** + * 멈춤 판단의 하한. 이보다 짧으면 글자 사이의 순간 멈춤에도 생성이 시작돼, + * 치는 도중 창이 떴다 사라지기를 반복한다(실측 2026-09-27: 100ms 설정에서 + * 5~6초마다 생성→소멸 반복). + */ + minTriggerDelayMs: 500, maxRequestsPerMinute: 6, dailyBudget: 500, /** 로컬 기억 경로(폴백)에서 한 번에 만드는 후보 수 — 채우기 루프가 없다. */