d3ro-voice/packages/core/__tests__/terminal-prompt.test.ts
Yun Chan 7e0c20b510 fix(suggestion): keep a shown suggestion while the user picks it, and suggest in terminals
A candidate list was cleared about two seconds after it appeared: once typing
paused, the not-typing rule dismissed the visible overlay, so a click or
Ctrl+Alt+Enter found nothing to insert. The rule now only stops new
requests; a visible overlay stays until it is accepted, dismissed or goes
stale.

Terminals were excluded from suggestions because the whole screen buffer
reads as the input and the last line is usually a status bar. The input line
is now extracted (Claude Code/Codex >, starship, PowerShell, cmd and POSIX
prompts, wrapped continuation lines) and used as the prefix; no prompt means
no suggestion. Terminals stay out of phrase learning. Accepting with nothing
shown and successful inserts are now logged.
2026-09-27 22:43:22 +09:00

76 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from 'vitest'
import { decideSuggestion, extractTerminalPromptPrefix, isTerminalApp } from '../src/input-intelligence'
describe('terminal prompt extraction', () => {
it('takes the Claude Code input line and ignores the status line below it', () => {
const screen = [
'● Wrote 3 files',
'',
'╭──────────────────────────────────────╮',
'│ > 오늘 회의 내용을 정리해서 │',
'╰──────────────────────────────────────╯',
' ◑ OPUS 5 · 5 medium │ CTX ▕░░▏',
].join('\n')
expect(extractTerminalPromptPrefix(screen)).toBe('오늘 회의 내용을 정리해서')
})
it('joins wrapped continuation lines of a long prompt', () => {
const screen = ['──────────', '> 긴 문장을 쓰다 보면', ' 줄이 바뀌어서 이어진다', '──────────', ' ? for shortcuts'].join('\n')
expect(extractTerminalPromptPrefix(screen)).toBe('긴 문장을 쓰다 보면 줄이 바뀌어서 이어진다')
})
it('reads shell prompts', () => {
expect(extractTerminalPromptPrefix('PS C:\\Users\\me> git commit -m "고친')).toBe('git commit -m "고친')
expect(extractTerminalPromptPrefix('C:\\work> dir')).toBe('dir')
expect(extractTerminalPromptPrefix('me@host:~/app$ npm run')).toBe('npm run')
expect(extractTerminalPromptPrefix('❯ cargo build')).toBe('cargo build')
})
it('returns null when only output is on screen, and does not mistake percentages for prompts', () => {
expect(extractTerminalPromptPrefix('Compiling...\n100% 완료\nDone in 3s')).toBeNull()
expect(extractTerminalPromptPrefix('')).toBeNull()
})
it('an empty prompt yields an empty prefix', () => {
expect(extractTerminalPromptPrefix('output\n> ')).toBe('')
})
it('recognises terminal apps', () => {
expect(isTerminalApp('WindowsTerminal.exe')).toBe(true)
expect(isTerminalApp('KakaoTalk.exe')).toBe(false)
expect(isTerminalApp(null)).toBe(false)
})
})
describe('a visible suggestion survives a typing pause', () => {
const base = {
enabled: true,
modelAvailable: true,
composing: false,
hasSelection: false,
isPassword: false,
isEditable: true,
appName: 'KakaoTalk.exe',
excludedApps: [],
editedSinceFocus: false,
typedRecently: false,
prefix: '내일 몇 시에 볼까',
idleMs: 5000,
triggerDelayMs: 100,
minPrefixChars: 4,
sinceLastRequestMs: 60_000,
minIntervalMs: 5000,
requestsThisMinute: 0,
maxRequestsPerMinute: 6,
requestsToday: 0,
dailyBudget: 500,
}
it('keeps the overlay while the user reads and picks a candidate', () => {
expect(decideSuggestion({ ...base, overlayVisible: true })).toEqual({ action: 'skip', reason: 'already-visible' })
})
it('still refuses to start a new suggestion without typing', () => {
expect(decideSuggestion({ ...base, overlayVisible: false })).toEqual({ action: 'clear', reason: 'not-typing' })
})
})