release: ship v1.6.0 with paged suggestions and a cleaner phrase memory
Some checks failed
deploy-site / deploy (push) Failing after 39s
release / release-windows (push) Failing after 3m41s
portable-unsigned / portable-windows (push) Failing after 12m23s

Next-sentence suggestions now arrive one at a time up to twelve, shown three
per page with Ctrl+Alt+Up/Down to move, Left/Right to page, Enter to accept
and Esc to close; old default bindings migrate and the panel guide follows the
live bindings. The overlay is redesigned, stays put while candidates stream
and sits outside the input box when no caret is reported.

The personal phrase memory stops learning from terminals, code editors and
the coding-agent hub, ignores symbol-heavy lines and empty-field placeholders,
and prunes existing entries that break those rules.

Fixes suggestion keys starting dictation, installs stuck on a pre-1.5.0
speech engine without the focus endpoint, Ollama runner windows flashing
while typing, the speech engine starting twice, and cold-model timeouts.
Live captions can be dragged to a remembered position and show a waiting
notice until the first line arrives.

Bumps the product version to 1.6.0 (Android/iOS build 1060000).
This commit is contained in:
Yun Chan 2026-09-24 19:56:28 +09:00
parent 856e375f3e
commit 2fe20fa7b5
71 changed files with 2469 additions and 366 deletions

View file

@ -33,6 +33,8 @@
* @param {{id: string, text: string, timestamp: number, isFinal: boolean}} segment
*/
function addSegment(segment) {
// 첫 자막이 오면 준비 안내를 걷는다 (그 전까지는 계속 보여 빈 화면을 만들지 않는다).
removeStatusLine()
// delta 줄이 있으면 제거 (확정 줄로 교체)
removeDeltaLine()
@ -172,26 +174,92 @@
})
// 상태 변경
// starting: 음성 모델을 준비하는 중
// active : 듣고 있지만 첫 자막은 아직 — 소리를 모아 첫 인식을 마칠 때까지 몇 초 걸린다.
// active 가 되자마자 안내를 지우면 그 몇 초가 빈 화면이라 "고장" 처럼 보였다.
window.popupAPI.on('caption:stateChanged', function (data) {
if (data._i18n) i18nStrings = data._i18n
if (data._i18n) {
i18nStrings = data._i18n
if (handleHint) handleHint.textContent = i18nStrings.captionDragHint || ''
}
if (data.state === 'starting') {
// 로딩 표시
clearAllLines()
var loadingEl = document.createElement('div')
loadingEl.className = 'caption-line loading'
loadingEl.id = 'caption-loading'
loadingEl.textContent = i18nStrings.captionLoading || '⏳ Loading STT model...'
loadingEl.style.fontSize = config.fontSize + 'px'
linesContainer.appendChild(loadingEl)
showStatusLine(i18nStrings.captionLoading)
} else if (data.state === 'active') {
// 로딩 표시 제거
var existing = document.getElementById('caption-loading')
if (existing && existing.parentNode) {
existing.parentNode.removeChild(existing)
}
if (lines.length === 0) showStatusLine(i18nStrings.captionWaiting)
} else if (data.state === 'inactive' || data.state === 'stopping') {
clearAllLines()
}
})
}
// ── 준비 안내 줄 ──────────────────────────────────────
function showStatusLine(text) {
if (!text) return
var el = document.getElementById('caption-loading')
if (!el) {
el = document.createElement('div')
el.className = 'caption-line loading'
el.id = 'caption-loading'
linesContainer.appendChild(el)
}
el.style.fontSize = config.fontSize + 'px'
el.textContent = text
}
function removeStatusLine() {
var el = document.getElementById('caption-loading')
if (el && el.parentNode) el.parentNode.removeChild(el)
}
// ── 끌어서 옮기기 ──────────────────────────────────────
// 창은 클릭 통과라 평소엔 아래 앱을 가리지 않는다. 마우스 이동은 전달받으므로
// 올라오면 손잡이를 보여 주고, 손잡이 위에서만 마우스를 받는다.
var root = document.getElementById('root')
var handle = document.getElementById('handle')
var handleHint = document.getElementById('handleHint')
var hoverTimer = null
var dragging = false
function send(channel, value) {
if (window.popupAPI) window.popupAPI.send(channel, value)
}
document.addEventListener('mousemove', function () {
if (!root) return
root.classList.add('hovering')
if (hoverTimer) clearTimeout(hoverTimer)
hoverTimer = setTimeout(function () {
if (!dragging) root.classList.remove('hovering')
}, 1500)
})
if (handle) {
handle.addEventListener('mouseenter', function () {
send('captionPopup:setInteractive', true)
})
handle.addEventListener('mouseleave', function () {
if (!dragging) send('captionPopup:setInteractive', false)
})
handle.addEventListener('pointerdown', function (event) {
if (event.button !== 0) return
dragging = true
handle.setPointerCapture(event.pointerId)
if (root) root.classList.add('dragging')
send('captionPopup:dragStart')
})
var finishDrag = function () {
if (!dragging) return
dragging = false
if (root) root.classList.remove('dragging')
send('captionPopup:dragEnd')
}
handle.addEventListener('pointerup', finishDrag)
handle.addEventListener('lostpointercapture', finishDrag)
handle.addEventListener('dblclick', function () {
send('captionPopup:resetPosition')
})
}
})()