From 1b8fe445f39c232ccf9f47d55fe624f610dfbd53 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sun, 27 Sep 2026 22:55:26 +0900 Subject: [PATCH] fix(suggestion): stop generating mid-typing and let a pause bring a suggestion at once With a 100 ms trigger left in the installed config, generation started in the gaps between keystrokes; the next key ended the session silently, and that session still counted against the 5 s minimum interval, so the moment the user actually paused nothing came (rate-limited). The log showed a generate-then-vanish cycle every 5-6 s. The trigger delay now has a 500 ms floor and settings revision 6 resets a stored value below it to 600 ms. A session ended because the user kept typing no longer blocks the next request by the minimum interval (per-minute and daily caps still apply), and that dismissal is logged. --- .../src/main/services/ConfigService.ts | 7 ++- .../src/main/services/SuggestionService.ts | 9 +++- .../tests/main/services/ConfigService.test.ts | 43 ++++++++++++++++++- .../main/services/input-flow-services.test.ts | 3 +- packages/core/src/input-intelligence.ts | 6 +++ 5 files changed, 63 insertions(+), 5 deletions(-) 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, /** 로컬 기억 경로(폴백)에서 한 번에 만드는 후보 수 — 채우기 루프가 없다. */