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.
This commit is contained in:
parent
7e0c20b510
commit
1b8fe445f3
5 changed files with 63 additions and 5 deletions
|
|
@ -18,7 +18,7 @@ import { getLogger } from './LoggerService'
|
||||||
const logger = getLogger('ConfigService')
|
const logger = getLogger('ConfigService')
|
||||||
|
|
||||||
/** 제안/텔레메트리 튜닝 기본값의 현재 개정판. 기본값을 바꾸면 올린다. */
|
/** 제안/텔레메트리 튜닝 기본값의 현재 개정판. 기본값을 바꾸면 올린다. */
|
||||||
const SUGGESTION_TUNING_REVISION = 5
|
const SUGGESTION_TUNING_REVISION = 6
|
||||||
|
|
||||||
const INITIAL_SUGGESTION_TUNING = {
|
const INITIAL_SUGGESTION_TUNING = {
|
||||||
suggestionTriggerDelayMs: 300,
|
suggestionTriggerDelayMs: 300,
|
||||||
|
|
@ -59,6 +59,11 @@ function migrateSuggestionTuning(activeStore: ElectronStore<AppConfig>): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current < 5) migrateSuggestionOverlayBindings(activeStore)
|
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)
|
activeStore.set('suggestionTuningRevision', SUGGESTION_TUNING_REVISION)
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
|
||||||
|
|
@ -356,7 +356,11 @@ class SuggestionService extends EventEmitter {
|
||||||
// 즉시 세션을 끝낸다(사용자 요청 사양: 계속 자라는 접두를 따라가지 않는다).
|
// 즉시 세션을 끝낸다(사용자 요청 사양: 계속 자라는 접두를 따라가지 않는다).
|
||||||
// 다음 멈춤에서 정책 게이트를 다시 거쳐 새 세션이 시작된다.
|
// 다음 멈춤에서 정책 게이트를 다시 거쳐 새 세션이 시작된다.
|
||||||
if (this.isVisible && !matchesSessionPrefix(this._generatedForPrefix, context.prefix)) {
|
if (this.isVisible && !matchesSessionPrefix(this._generatedForPrefix, context.prefix)) {
|
||||||
|
logger.debug('제안 닫음: 이어서 입력함 — 멈추면 새 문맥으로 다시 만든다')
|
||||||
this.dismiss('stale')
|
this.dismiss('stale')
|
||||||
|
// 사용자가 고르지 않고 계속 쳤다 — 이 세션 때문에 최소 간격(5초)에 걸려
|
||||||
|
// 정작 멈췄을 때 아무것도 안 뜨면 안 된다. 분당·일일 상한은 그대로 둔다.
|
||||||
|
this._lastRequestAt = Math.min(this._lastRequestAt, now - SUGGESTION_DEFAULTS.minIntervalMs)
|
||||||
this.emit('state-changed', this.getState())
|
this.emit('state-changed', this.getState())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -1330,7 +1334,10 @@ class SuggestionService extends EventEmitter {
|
||||||
excludedApps: string[]
|
excludedApps: string[]
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
triggerDelayMs: configGet('suggestionTriggerDelayMs') || SUGGESTION_DEFAULTS.triggerDelayMs,
|
triggerDelayMs: Math.max(
|
||||||
|
SUGGESTION_DEFAULTS.minTriggerDelayMs,
|
||||||
|
configGet('suggestionTriggerDelayMs') || SUGGESTION_DEFAULTS.triggerDelayMs
|
||||||
|
),
|
||||||
minPrefixChars: configGet('suggestionMinPrefixChars') || SUGGESTION_DEFAULTS.minPrefixChars,
|
minPrefixChars: configGet('suggestionMinPrefixChars') || SUGGESTION_DEFAULTS.minPrefixChars,
|
||||||
maxRequestsPerMinute:
|
maxRequestsPerMinute:
|
||||||
Math.min(
|
Math.min(
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ describe('ConfigService suggestion tuning migration', () => {
|
||||||
)
|
)
|
||||||
await initConfigService()
|
await initConfigService()
|
||||||
|
|
||||||
expect(configGet('suggestionTuningRevision')).toBe(5)
|
expect(configGet('suggestionTuningRevision')).toBe(6)
|
||||||
expect(configGet('suggestionTriggerDelayMs')).toBe(600)
|
expect(configGet('suggestionTriggerDelayMs')).toBe(600)
|
||||||
expect(configGet('suggestionMaxRequestsPerMinute')).toBe(6)
|
expect(configGet('suggestionMaxRequestsPerMinute')).toBe(6)
|
||||||
expect(configGet('suggestionMinPrefixChars')).toBe(17)
|
expect(configGet('suggestionMinPrefixChars')).toBe(17)
|
||||||
|
|
@ -114,7 +114,7 @@ describe('ConfigService suggestion tuning migration', () => {
|
||||||
// 옮기지 않는 액션은 그대로 남는다.
|
// 옮기지 않는 액션은 그대로 남는다.
|
||||||
expect(bindings['suggestion-next']).toEqual(OLD_NEXT)
|
expect(bindings['suggestion-next']).toEqual(OLD_NEXT)
|
||||||
expect(bindings['suggestion-prev']).toEqual(OLD_PREV)
|
expect(bindings['suggestion-prev']).toEqual(OLD_PREV)
|
||||||
expect(configGet('suggestionTuningRevision')).toBe(5)
|
expect(configGet('suggestionTuningRevision')).toBe(6)
|
||||||
|
|
||||||
resetInMemoryConfig()
|
resetInMemoryConfig()
|
||||||
})
|
})
|
||||||
|
|
@ -144,4 +144,43 @@ describe('ConfigService suggestion tuning migration', () => {
|
||||||
resetInMemoryConfig()
|
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<T extends Record<string, unknown>> {
|
||||||
|
store: T
|
||||||
|
|
||||||
|
constructor(options: { defaults: T }) {
|
||||||
|
this.store = { ...options.defaults, ...persisted } as T
|
||||||
|
}
|
||||||
|
|
||||||
|
get<K extends keyof T>(key: K): T[K] {
|
||||||
|
return this.store[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
set<K extends keyof T>(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()
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -477,7 +477,8 @@ describe('입력 플로우 서비스', () => {
|
||||||
available: true,
|
available: true,
|
||||||
appName: 'Notion.exe',
|
appName: 'Notion.exe',
|
||||||
windowTitle: '회의록',
|
windowTitle: '회의록',
|
||||||
idleMs: 300,
|
// 멈춤 판단 하한(500ms)을 넘긴 멈춤
|
||||||
|
idleMs: 700,
|
||||||
capturedAt: now,
|
capturedAt: now,
|
||||||
editedSinceFocus: true,
|
editedSinceFocus: true,
|
||||||
typedRecently: true
|
typedRecently: true
|
||||||
|
|
|
||||||
|
|
@ -1276,6 +1276,12 @@ export const SUGGESTION_DEFAULTS = {
|
||||||
*/
|
*/
|
||||||
minPrefixChars: 8,
|
minPrefixChars: 8,
|
||||||
minIntervalMs: 5000,
|
minIntervalMs: 5000,
|
||||||
|
/**
|
||||||
|
* 멈춤 판단의 하한. 이보다 짧으면 글자 사이의 순간 멈춤에도 생성이 시작돼,
|
||||||
|
* 치는 도중 창이 떴다 사라지기를 반복한다(실측 2026-09-27: 100ms 설정에서
|
||||||
|
* 5~6초마다 생성→소멸 반복).
|
||||||
|
*/
|
||||||
|
minTriggerDelayMs: 500,
|
||||||
maxRequestsPerMinute: 6,
|
maxRequestsPerMinute: 6,
|
||||||
dailyBudget: 500,
|
dailyBudget: 500,
|
||||||
/** 로컬 기억 경로(폴백)에서 한 번에 만드는 후보 수 — 채우기 루프가 없다. */
|
/** 로컬 기억 경로(폴백)에서 한 번에 만드는 후보 수 — 채우기 루프가 없다. */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue