feat(polish): allow mood emoji on custom styles and ship Flow/ASR fixes

Custom polish styles can opt in to emotion-matched emoji (default off), with
prompt-level opt-in detection so paste-only styles keep model-added emoji.
Also include Volcengine API-Key ASR auth, voice-processing capture, PiP flash
fix, and related keyboard Shift/haptics reliability work.
This commit is contained in:
Rocky
2026-08-06 15:11:12 +08:00
parent 2ed16e1fbf
commit a8d58d8f0c
47 changed files with 1467 additions and 258 deletions
@@ -61,6 +61,15 @@ final class EnglishTypingTests: XCTestCase {
XCTAssertTrue(
TypingAutocapitalization.shouldCapitalize(precedingText: "Hello!\n", mode: .sentences)
)
// System keyboard (.sentences): Return starts a new line capitalize.
// Notes-style document context after Return is typically "\n".
XCTAssertTrue(
TypingAutocapitalization.shouldCapitalize(precedingText: "Hello\n", mode: .sentences),
"Return/newline must arm Shift (Notes multi-line)"
)
XCTAssertTrue(
TypingAutocapitalization.shouldCapitalize(precedingText: "Hello\n\n", mode: .sentences)
)
XCTAssertFalse(
TypingAutocapitalization.shouldCapitalize(precedingText: "Hello ", mode: .sentences)
)
@@ -72,6 +81,55 @@ final class EnglishTypingTests: XCTestCase {
)
}
@MainActor
func testEnglishShiftArmsAfterReturnLikeNotes() {
// Simulates Notes: proxy preceding text gains a trailing newline after Return.
var preceding = "Hello"
let typing = TypingSessionController()
typing.precedingTextProvider = { preceding }
typing.autocapitalizationModeProvider = { .sentences }
_ = typing.setLanguage(.english)
XCTAssertFalse(typing.shiftActive, "mid-word should not arm Shift")
_ = typing.handleReturn()
preceding = "Hello\n" // host document after inserting newline
typing.syncAutocapitalization(accountingForInsert: "\n")
XCTAssertTrue(
typing.shiftActive,
"after Return, Shift must light for next line (system sentences behavior)"
)
XCTAssertEqual(typing.keyRows.first?.first, "Q")
}
@MainActor
func testEnglishShiftArmsWhenProxyLagsAfterReturn() {
// Notes often still reports pre-Return context right after insertText("\n").
var preceding = "Hello"
let typing = TypingSessionController()
typing.precedingTextProvider = { preceding }
typing.autocapitalizationModeProvider = { .sentences }
_ = typing.setLanguage(.english)
_ = typing.handleReturn()
// Proxy intentionally stale still "Hello" without "\n".
typing.syncAutocapitalization(accountingForInsert: "\n")
XCTAssertTrue(typing.shiftActive)
XCTAssertEqual(typing.keyRows.first?.first, "Q")
}
@MainActor
func testEnglishShiftArmsWhenProxyLagsAfterPeriod() {
var preceding = "Hello"
let typing = TypingSessionController()
typing.precedingTextProvider = { preceding }
typing.autocapitalizationModeProvider = { .sentences }
_ = typing.setLanguage(.english)
_ = typing.handleKey(".")
typing.syncAutocapitalization(accountingForInsert: ".")
XCTAssertTrue(typing.shiftActive)
}
@MainActor
func testEnglishIdleShowsNoCandidatesUntilLetterTyped() {
let typing = TypingSessionController()
@@ -154,4 +154,120 @@ final class KeyboardSurfaceStateTests: XCTestCase {
XCTAssertFalse(typing.shiftHeld)
XCTAssertFalse(typing.capsLock)
}
func testChineseShiftInsertsUppercaseLatinBypassingRime() {
let engine = TrackingStubRimeEngine()
let typing = TypingSessionController(engine: { engine })
_ = typing.handleKey("")
XCTAssertTrue(typing.shiftActive)
let output = typing.handleKey("N")
XCTAssertEqual(output, .insert("N"))
XCTAssertEqual(engine.processCharacterCallCount, 0)
XCTAssertFalse(typing.shiftActive, "one-shot Shift clears after Latin insert")
XCTAssertTrue(typing.composition.preedit.isEmpty)
}
func testChineseShiftPreservesExistingComposition() {
let engine = TrackingStubRimeEngine()
let typing = TypingSessionController(engine: { engine })
// Seed session composition via a lowercase letter (goes to Rime).
_ = typing.handleKey("n")
XCTAssertEqual(typing.composition.preedit, "n")
_ = typing.handleKey("")
let output = typing.handleKey("A")
XCTAssertEqual(output, .insert("A"))
XCTAssertEqual(engine.processCharacterCallCount, 1, "only the unshifted letter hits Rime")
XCTAssertEqual(typing.composition.preedit, "n", "Shift Latin must not clear preedit")
}
func testChineseCapsLockKeepsInsertingUppercaseLatin() {
let engine = TrackingStubRimeEngine()
let typing = TypingSessionController(engine: { engine })
_ = typing.handleKey("")
_ = typing.handleKey("") // second tap Caps Lock
XCTAssertTrue(typing.capsLock)
XCTAssertEqual(typing.handleKey("O"), .insert("O"))
XCTAssertEqual(typing.handleKey("S"), .insert("S"))
XCTAssertEqual(engine.processCharacterCallCount, 0)
XCTAssertTrue(typing.capsLock)
}
func testChineseUnshiftedLetterStillComposes() {
let engine = TrackingStubRimeEngine()
let typing = TypingSessionController(engine: { engine })
let output = typing.handleKey("n")
XCTAssertEqual(output, .none)
XCTAssertEqual(engine.processCharacterCallCount, 1)
XCTAssertEqual(engine.lastProcessedCharacter, "n")
XCTAssertEqual(typing.composition.preedit, "n")
}
}
/// Stub that records `processCharacter` calls for Chinese Shift bypass tests.
@MainActor
private final class TrackingStubRimeEngine: RimeEngineBridging {
var composition: TypingComposition = .empty
var isReady: Bool = true
var schema: TypingInputSchema = .fullPinyin
private(set) var processCharacterCallCount = 0
private(set) var lastProcessedCharacter: Character?
func prepare() async throws {}
func teardown() { composition = .empty }
func setLanguage(_ language: TypingInputLanguage) {
if language == .english { composition = .empty }
}
@discardableResult
func setSchema(_ schema: TypingInputSchema) -> Bool {
self.schema = schema
return true
}
func processCharacter(_ character: Character) -> String? {
processCharacterCallCount += 1
lastProcessedCharacter = character
composition = TypingComposition(
preedit: String(character).lowercased(),
candidates: [TypingCandidate(text: "")]
)
return nil
}
func processBackspace() -> String? {
composition = .empty
return nil
}
func processSpace() -> String? {
composition = .empty
return " "
}
func processReturn() -> String? {
composition = .empty
return "\n"
}
func selectCandidate(at index: Int) -> String {
composition = .empty
return ""
}
func flushPreedit() -> String {
let raw = composition.preedit
composition = .empty
return raw
}
func clearComposition() {
composition = .empty
}
}