Cursor: Apply local changes for cloud agent
This commit is contained in:
@@ -192,6 +192,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
recordMemory("KVC.viewDidLoad.afterInstallSwiftUI")
|
||||
OSGDiag.log("KVC.viewDidLoad after installSwiftUI \(OSGDiag.memoryTag())", category: "boot")
|
||||
let configLoadResult = configSync.loadPersistedConfig()
|
||||
refreshMultipleReplyVariantsSetting()
|
||||
recordMemory(
|
||||
"KVC.viewDidLoad.afterConfigLoad",
|
||||
details: "result=\(configLoadResult)"
|
||||
@@ -279,6 +280,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
flowCoordinator.startSessionMonitor()
|
||||
configSync.syncOnboardingStateFromAppGroup()
|
||||
configSync.refreshConfigFromAppGroup()
|
||||
refreshMultipleReplyVariantsSetting()
|
||||
clipboardCapture.refreshFlagsFromStore()
|
||||
// Settings may have changed while the extension stayed alive.
|
||||
applyPreferredSurfaceOnOpen()
|
||||
@@ -456,6 +458,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
// error; never force-create one just to retry.
|
||||
self?.typingSessionStorage?.reloadPersonalDictionaryTerms()
|
||||
self?.typingSessionStorage?.retryPrepareAfterResourceDeployment()
|
||||
self?.refreshMultipleReplyVariantsSetting()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -594,6 +597,9 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
state.confirmPendingAIAnswer = { [weak self] in
|
||||
self?.aiKeyboardCoordinator.confirmPendingAnswer()
|
||||
}
|
||||
state.selectAIReplyVariant = { [weak self] id in
|
||||
self?.aiKeyboardCoordinator.selectReplyVariant(id: id)
|
||||
}
|
||||
state.discardPendingAIAnswer = { [weak self] in
|
||||
self?.aiKeyboardCoordinator.discardPendingAnswer()
|
||||
}
|
||||
@@ -635,6 +641,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
}
|
||||
state.clearClipboardHistory = { [weak self] in
|
||||
self?.clipboardCapture.clearHistory()
|
||||
ClipboardReplyFeedbackStore.shared.clear()
|
||||
}
|
||||
state.deleteClipboardHistoryEntry = { [weak self] id in
|
||||
self?.clipboardCapture.deleteEntry(id: id)
|
||||
@@ -694,6 +701,12 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
/// This App Group property is supplied by the parallel settings module.
|
||||
/// Reading it here keeps the shared state independent from persistence.
|
||||
private func refreshMultipleReplyVariantsSetting() {
|
||||
state.multipleReplyVariantsEnabled = AppGroupStore().multipleReplyVariantsEnabled
|
||||
}
|
||||
|
||||
private func applySurface(_ requestedSurface: State.Surface) {
|
||||
// `.ai` is retained only to decode preferences written by older builds.
|
||||
// The product now has one unified assistant surface.
|
||||
|
||||
@@ -18,10 +18,16 @@ final class AIKeyboardCoordinator {
|
||||
private let openWebURL: (URL) -> Void
|
||||
private let callPhone: (String) -> Void
|
||||
private let createContact: (String) -> Void
|
||||
private let replyFeedbackStore: ClipboardReplyFeedbackStore
|
||||
private var requestInsertionFingerprint: String?
|
||||
private var conversationInsertionFingerprint: String?
|
||||
private var hasConversationInsertionTarget = false
|
||||
private var requestOOBEFeature: ManagedGatewayOOBEFeature?
|
||||
private var requestExpectsReplyVariants = false
|
||||
private var requestReplySourceText: String?
|
||||
private var requestReplyFeedbackSource: String?
|
||||
private var pendingReplyFeedbackRecordID: UUID?
|
||||
private var pendingStructuredReplyResult = false
|
||||
|
||||
init(
|
||||
state: KeyboardState,
|
||||
@@ -31,7 +37,8 @@ final class AIKeyboardCoordinator {
|
||||
captureInsertionFingerprint: @escaping () -> String?,
|
||||
openWebURL: @escaping (URL) -> Void,
|
||||
callPhone: @escaping (String) -> Void,
|
||||
createContact: @escaping (String) -> Void
|
||||
createContact: @escaping (String) -> Void,
|
||||
replyFeedbackStore: ClipboardReplyFeedbackStore = .shared
|
||||
) {
|
||||
self.state = state
|
||||
self.flow = flow
|
||||
@@ -41,13 +48,19 @@ final class AIKeyboardCoordinator {
|
||||
self.openWebURL = openWebURL
|
||||
self.callPhone = callPhone
|
||||
self.createContact = createContact
|
||||
self.replyFeedbackStore = replyFeedbackStore
|
||||
}
|
||||
|
||||
func beginNewPresentation() {
|
||||
discardPendingReplyFeedback()
|
||||
endConversationIfNeeded()
|
||||
state.aiSession.enter()
|
||||
requestInsertionFingerprint = nil
|
||||
requestOOBEFeature = nil
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplySourceText = nil
|
||||
requestReplyFeedbackSource = nil
|
||||
pendingStructuredReplyResult = false
|
||||
conversationInsertionFingerprint = nil
|
||||
hasConversationInsertionTarget = false
|
||||
}
|
||||
@@ -61,10 +74,15 @@ final class AIKeyboardCoordinator {
|
||||
if state.aiSession.isBusy {
|
||||
flow.cancelAIRecording()
|
||||
}
|
||||
discardPendingReplyFeedback()
|
||||
endConversationIfNeeded()
|
||||
state.aiSession.leave()
|
||||
requestInsertionFingerprint = nil
|
||||
requestOOBEFeature = nil
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplySourceText = nil
|
||||
requestReplyFeedbackSource = nil
|
||||
pendingStructuredReplyResult = false
|
||||
conversationInsertionFingerprint = nil
|
||||
hasConversationInsertionTarget = false
|
||||
}
|
||||
@@ -143,6 +161,10 @@ final class AIKeyboardCoordinator {
|
||||
}
|
||||
|
||||
prepareConversationForRequest()
|
||||
requestReplyFeedbackSource = skill.id == AIClipboardSkillCatalog.replyID
|
||||
&& oobeFeature == nil
|
||||
? material
|
||||
: nil
|
||||
if skill.kind == .export {
|
||||
state.pendingClipboardSkillID = skill.id
|
||||
state.pendingClipboardSkillSource = material
|
||||
@@ -155,6 +177,12 @@ final class AIKeyboardCoordinator {
|
||||
translationTargetLocaleId: state.translationTargetLocaleId,
|
||||
replyStyle: state.clipboardReplyStyle
|
||||
)
|
||||
let expectsReplyVariants = state.multipleReplyVariantsEnabled
|
||||
&& skill.id == AIClipboardSkillCatalog.replyID
|
||||
requestReplySourceText = expectsReplyVariants ? material : nil
|
||||
if expectsReplyVariants {
|
||||
instruction += "\n\(replyVariantsOutputContract())"
|
||||
}
|
||||
if skill.kind == .export {
|
||||
instruction += "\nPreserve the source language, addresses, names, and proper nouns."
|
||||
}
|
||||
@@ -196,7 +224,8 @@ final class AIKeyboardCoordinator {
|
||||
resolution,
|
||||
taskKind: skill.managedGatewayTaskKind,
|
||||
oobeFeature: oobeFeature,
|
||||
thinkingEnabled: skill.thinkingEnabled
|
||||
thinkingEnabled: skill.thinkingEnabled,
|
||||
expectsReplyVariants: expectsReplyVariants
|
||||
)
|
||||
}
|
||||
|
||||
@@ -236,7 +265,8 @@ final class AIKeyboardCoordinator {
|
||||
requestSource: ManagedGatewayRequestSource? = nil,
|
||||
oobeFeature: ManagedGatewayOOBEFeature? = nil,
|
||||
thinkingEnabled: Bool? = nil,
|
||||
webPageURL: URL? = nil
|
||||
webPageURL: URL? = nil,
|
||||
expectsReplyVariants: Bool = false
|
||||
) {
|
||||
guard case .ready(let prompt) = resolution else {
|
||||
// The clipboard window closed between rendering and this tap.
|
||||
@@ -252,6 +282,7 @@ final class AIKeyboardCoordinator {
|
||||
return
|
||||
}
|
||||
requestOOBEFeature = oobeFeature
|
||||
requestExpectsReplyVariants = expectsReplyVariants
|
||||
let disposition = flow.submitAIQuestion(
|
||||
text: prompt,
|
||||
conversationID: conversationID,
|
||||
@@ -263,6 +294,9 @@ final class AIKeyboardCoordinator {
|
||||
)
|
||||
if case .rejected(let rejection) = disposition {
|
||||
clearPendingExportSkill()
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplySourceText = nil
|
||||
requestReplyFeedbackSource = nil
|
||||
state.aiSession.fail(message(for: rejection), utteranceID: nil)
|
||||
}
|
||||
}
|
||||
@@ -272,6 +306,8 @@ final class AIKeyboardCoordinator {
|
||||
clearPendingExportSkill()
|
||||
requestInsertionFingerprint = nil
|
||||
requestOOBEFeature = nil
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplySourceText = nil
|
||||
flow.cancelAIRecording()
|
||||
state.aiSession.cancelCurrentWork()
|
||||
}
|
||||
@@ -279,19 +315,38 @@ final class AIKeyboardCoordinator {
|
||||
func confirmPendingAnswer() {
|
||||
if state.aiSession.canInsert, let answer = state.aiSession.answer {
|
||||
guard insertAnswer(answer) else { return }
|
||||
recordReplySelection(answer: answer)
|
||||
markOOBECompletedAfterInsertion()
|
||||
state.aiSession.markAnswerInserted(
|
||||
offersSend: state.returnKeyRole.usesActionFill
|
||||
)
|
||||
conversationInsertionFingerprint = captureInsertionFingerprint()
|
||||
hasConversationInsertionTarget = true
|
||||
resetStructuredReplyConversationIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func selectReplyVariant(id: UUID) {
|
||||
guard let answer = state.aiSession.selectReplyVariant(id: id),
|
||||
insertAnswer(answer) else {
|
||||
return
|
||||
}
|
||||
recordReplySelection(answer: answer)
|
||||
markOOBECompletedAfterInsertion()
|
||||
state.aiSession.markAnswerInserted(
|
||||
offersSend: state.returnKeyRole.usesActionFill
|
||||
)
|
||||
conversationInsertionFingerprint = captureInsertionFingerprint()
|
||||
hasConversationInsertionTarget = true
|
||||
resetStructuredReplyConversationIfNeeded()
|
||||
}
|
||||
|
||||
func discardPendingAnswer() {
|
||||
discardPendingReplyFeedback()
|
||||
state.aiSession.discardReadyAnswer()
|
||||
requestInsertionFingerprint = nil
|
||||
requestOOBEFeature = nil
|
||||
resetStructuredReplyConversationIfNeeded()
|
||||
}
|
||||
|
||||
func performCurrentFieldAction() {
|
||||
@@ -343,7 +398,7 @@ final class AIKeyboardCoordinator {
|
||||
}
|
||||
|
||||
func receivePartialAnswer(_ draft: String, utteranceID: UUID) {
|
||||
if isPendingExportSkill { return }
|
||||
if isPendingExportSkill || requestExpectsReplyVariants { return }
|
||||
state.aiSession.receivePartialAnswer(draft, utteranceID: utteranceID)
|
||||
}
|
||||
|
||||
@@ -359,13 +414,55 @@ final class AIKeyboardCoordinator {
|
||||
guard result.aiConversationID == state.aiSession.conversationID,
|
||||
let answer = result.text,
|
||||
!answer.isEmpty else {
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplySourceText = nil
|
||||
requestReplyFeedbackSource = nil
|
||||
state.aiSession.fail(
|
||||
ExtL10n.string("keyboard.ai.error.requestFailed"),
|
||||
utteranceID: result.utteranceId
|
||||
)
|
||||
return
|
||||
}
|
||||
if requestExpectsReplyVariants {
|
||||
requestExpectsReplyVariants = false
|
||||
requestInsertionFingerprint = nil
|
||||
let sourceText = requestReplySourceText
|
||||
requestReplySourceText = nil
|
||||
switch AIReplyVariantParser.parseOrFallback(
|
||||
answer,
|
||||
sourceText: sourceText
|
||||
) {
|
||||
case .variants(let variants):
|
||||
state.aiSession.receiveReplyVariants(
|
||||
variants,
|
||||
utteranceID: result.utteranceId
|
||||
)
|
||||
beginReplyFeedback(variants: variants)
|
||||
pendingStructuredReplyResult = true
|
||||
case .single(let fallback):
|
||||
// A malformed structured response stays explicit-review only.
|
||||
// Never auto-insert model JSON or a code fence into the host.
|
||||
state.aiSession.receiveAnswer(
|
||||
fallback.text,
|
||||
utteranceID: result.utteranceId
|
||||
)
|
||||
if let answer = state.aiSession.answer {
|
||||
beginReplyFeedback(answer: answer)
|
||||
}
|
||||
pendingStructuredReplyResult = true
|
||||
case nil:
|
||||
requestReplyFeedbackSource = nil
|
||||
state.aiSession.fail(
|
||||
ExtL10n.string("keyboard.ai.error.requestFailed"),
|
||||
utteranceID: result.utteranceId
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
state.aiSession.receiveAnswer(answer, utteranceID: result.utteranceId)
|
||||
if let answer = state.aiSession.answer {
|
||||
beginReplyFeedback(answer: answer)
|
||||
}
|
||||
defer { requestInsertionFingerprint = nil }
|
||||
guard state.aiSession.canInsert,
|
||||
let expected = requestInsertionFingerprint,
|
||||
@@ -376,6 +473,7 @@ final class AIKeyboardCoordinator {
|
||||
// fallback when the field or caret changed during generation.
|
||||
return
|
||||
}
|
||||
recordReplySelection(answer: answer)
|
||||
markOOBECompletedAfterInsertion()
|
||||
state.aiSession.markAnswerInserted(
|
||||
offersSend: state.returnKeyRole.usesActionFill
|
||||
@@ -388,6 +486,9 @@ final class AIKeyboardCoordinator {
|
||||
clearPendingExportSkill()
|
||||
requestInsertionFingerprint = nil
|
||||
requestOOBEFeature = nil
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplySourceText = nil
|
||||
requestReplyFeedbackSource = nil
|
||||
state.aiSession.fail(message, utteranceID: utteranceID)
|
||||
}
|
||||
|
||||
@@ -410,6 +511,10 @@ final class AIKeyboardCoordinator {
|
||||
}
|
||||
|
||||
private func prepareConversationForRequest() {
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplySourceText = nil
|
||||
requestReplyFeedbackSource = nil
|
||||
pendingStructuredReplyResult = false
|
||||
let currentFingerprint = captureInsertionFingerprint()
|
||||
if state.aiSession.isActive,
|
||||
hasConversationInsertionTarget,
|
||||
@@ -423,6 +528,95 @@ final class AIKeyboardCoordinator {
|
||||
requestInsertionFingerprint = currentFingerprint
|
||||
}
|
||||
|
||||
private func beginReplyFeedback(variants: [AIReplyVariant]) {
|
||||
guard let sourceText = requestReplyFeedbackSource else { return }
|
||||
requestReplyFeedbackSource = nil
|
||||
let snapshots = variants.map { variant in
|
||||
ClipboardReplyCandidateSnapshot(
|
||||
id: variant.id,
|
||||
kind: feedbackKind(for: variant.kind),
|
||||
text: variant.text,
|
||||
emotion: variant.emotion.rawValue
|
||||
)
|
||||
}
|
||||
pendingReplyFeedbackRecordID = replyFeedbackStore.begin(
|
||||
sourceText: sourceText,
|
||||
candidates: snapshots,
|
||||
styleID: state.clipboardReplyStyle?.styleID
|
||||
)
|
||||
}
|
||||
|
||||
private func beginReplyFeedback(answer: AIAnswer) {
|
||||
beginReplyFeedback(
|
||||
variants: [
|
||||
AIReplyVariant(
|
||||
id: answer.id,
|
||||
kind: .ordinary,
|
||||
emotion: .neutral,
|
||||
text: answer.text
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
private func recordReplySelection(answer: AIAnswer) {
|
||||
guard let recordID = pendingReplyFeedbackRecordID else { return }
|
||||
let candidateID = state.aiSession.selectedReplyVariant?.id ?? answer.id
|
||||
replyFeedbackStore.recordSelection(
|
||||
recordID: recordID,
|
||||
candidateID: candidateID,
|
||||
answerID: answer.id
|
||||
)
|
||||
pendingReplyFeedbackRecordID = nil
|
||||
}
|
||||
|
||||
private func discardPendingReplyFeedback() {
|
||||
guard let recordID = pendingReplyFeedbackRecordID else { return }
|
||||
replyFeedbackStore.recordDiscard(recordID: recordID)
|
||||
pendingReplyFeedbackRecordID = nil
|
||||
}
|
||||
|
||||
private func feedbackKind(
|
||||
for kind: AIReplyVariant.Kind
|
||||
) -> ClipboardReplyCandidateSnapshot.Kind {
|
||||
switch kind {
|
||||
case .ordinary:
|
||||
return .ordinary
|
||||
case .formal:
|
||||
return .formal
|
||||
case .playful:
|
||||
return .playful
|
||||
}
|
||||
}
|
||||
|
||||
/// The host conversation contains the structured JSON result rather than
|
||||
/// the chosen reply. Start a clean in-memory turn after selection/discard
|
||||
/// so a later spoken follow-up cannot treat that JSON as chat history.
|
||||
private func resetStructuredReplyConversationIfNeeded() {
|
||||
guard pendingStructuredReplyResult,
|
||||
state.aiSession.isActive else {
|
||||
return
|
||||
}
|
||||
pendingStructuredReplyResult = false
|
||||
endConversationIfNeeded()
|
||||
state.aiSession.resetConversationPreservingAnswer()
|
||||
}
|
||||
|
||||
private func replyVariantsOutputContract() -> String {
|
||||
"""
|
||||
MULTI-REPLY OUTPUT CONTRACT (highest priority):
|
||||
Return only one valid JSON object with exactly this shape and no Markdown fence or extra keys:
|
||||
{"variants":[{"kind":"ordinary","emotion":"neutral","text":"..."},{"kind":"formal","emotion":"neutral","text":"..."},{"kind":"playful","emotion":"playful","text":"..."}]}
|
||||
Include exactly one ordinary, one formal, and one playful item in that order. Every text must be a complete reply in the source language. All three must keep the same semantic stance, facts, and level of commitment. If the source does not establish whether the user should accept, decline, promise, schedule, or otherwise decide, do not invent that decision; stay neutral or ask for the missing detail.
|
||||
Every item must advance the conversation with a reaction, answer, question, decision, or next step. Never restate, paraphrase, summarize, or synonymically rewrite the clipboard text. In particular, do not begin a reply by repeating the source's subject and event. For a declarative update, react to its implication or emotion instead of reporting the update back to its sender.
|
||||
Apply the existing <user_reply_style> wording, rhythm, and stable habits to every item without changing these rules.
|
||||
ordinary: natural for the situation; add emoji only when context makes it useful.
|
||||
formal: professional and natural; add no new emoji by default.
|
||||
playful: relaxed and fun. Emoji has no fixed numeric cap, may be varied when context supports it, must not become meaningless stacking, and must not default to using only 😂. This playful emoji rule overrides any personal no-emoji preference.
|
||||
emotion must be exactly one of: neutral, warm, celebratory, empathetic, encouraging, grateful, apologetic, reassuring, playful, enthusiastic, calm. The app, not the model, chooses all icons.
|
||||
"""
|
||||
}
|
||||
|
||||
private func oobeFeature(for skill: AIClipboardSkill) -> ManagedGatewayOOBEFeature? {
|
||||
switch skill.id {
|
||||
case AIClipboardSkillCatalog.replyID:
|
||||
|
||||
@@ -76,6 +76,8 @@ struct AIKeyboardView: View {
|
||||
Group {
|
||||
if state.editSession.isActive {
|
||||
LastInputEditView(state: state)
|
||||
} else if state.aiSession.canSelectReplyVariant {
|
||||
replyVariantsSurface
|
||||
} else if state.aiSession.canInsert {
|
||||
pendingAnswerSurface
|
||||
} else {
|
||||
@@ -204,6 +206,79 @@ struct AIKeyboardView: View {
|
||||
.frame(height: resolvedHeight)
|
||||
}
|
||||
|
||||
private var replyVariantsSurface: some View {
|
||||
VStack(spacing: 0) {
|
||||
topBar.frame(height: Layout.topBarHeight)
|
||||
ScrollView(.vertical) {
|
||||
LazyVStack(spacing: Spacing.xs) {
|
||||
ForEach(state.aiSession.replyVariants) { variant in
|
||||
replyVariantButton(variant)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, Spacing.xs)
|
||||
.padding(.vertical, Spacing.xs)
|
||||
}
|
||||
.scrollIndicators(.visible)
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.accessibilityIdentifier("assistant.replyVariants")
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal, KeyboardChromeLayout.horizontalInset)
|
||||
.frame(maxWidth: KeyboardChromeLayout.voiceContentMaxWidth)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: resolvedHeight)
|
||||
}
|
||||
|
||||
private func replyVariantButton(_ variant: AIReplyVariant) -> some View {
|
||||
let title = ExtL10n.string(variant.kind.titleKey)
|
||||
let shape = RoundedRectangle(cornerRadius: Radius.medium)
|
||||
return Button {
|
||||
state.selectAIReplyVariant(variant.id)
|
||||
} label: {
|
||||
HStack(alignment: .top, spacing: Spacing.sm) {
|
||||
Image(
|
||||
systemName: variant.emotion.systemImage(
|
||||
fallback: variant.kind
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.symbolRenderingMode(.monochrome)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
.frame(width: 17, height: 17)
|
||||
.frame(width: 24, height: 24)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.lineLimit(1)
|
||||
Text(variant.text)
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(.horizontal, Spacing.sm)
|
||||
.padding(.vertical, 10)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(shape)
|
||||
.glassEffect(
|
||||
.regular
|
||||
.tint(palette.textPrimary.opacity(0.04))
|
||||
.interactive(),
|
||||
in: shape
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityIdentifier("assistant.replyVariant.\(variant.kind.rawValue)")
|
||||
.accessibilityLabel(Text("\(title), \(variant.text)"))
|
||||
.accessibilityHint(ExtL10n.text("keyboard.ai.replyVariant.insertHint"))
|
||||
}
|
||||
|
||||
private var resolvedHeight: CGFloat {
|
||||
TypingSurfaceMetrics.contentHeight(
|
||||
isIPad: state.usesIPadLayoutMetrics,
|
||||
@@ -215,7 +290,7 @@ struct AIKeyboardView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var topBar: some View {
|
||||
if state.aiSession.canInsert {
|
||||
if state.aiSession.canInsert || state.aiSession.canSelectReplyVariant {
|
||||
cancelTopBar(
|
||||
action: state.discardPendingAIAnswer,
|
||||
labelKey: "keyboard.assistant.discardPending",
|
||||
@@ -1060,7 +1135,11 @@ struct AIKeyboardView: View {
|
||||
// MARK: - Visibility and clipboard
|
||||
|
||||
private var assistantIsResting: Bool {
|
||||
guard !state.aiSession.isBusy, !state.aiSession.canInsert else { return false }
|
||||
guard !state.aiSession.isBusy,
|
||||
!state.aiSession.canInsert,
|
||||
!state.aiSession.canSelectReplyVariant else {
|
||||
return false
|
||||
}
|
||||
switch state.phase {
|
||||
case .idle, .error, .denied:
|
||||
return true
|
||||
|
||||
@@ -311,6 +311,10 @@
|
||||
"keyboard.assistant.insertPending" = "Insert retained AI answer";
|
||||
"keyboard.assistant.discardPending" = "Discard AI answer";
|
||||
"keyboard.assistant.discardPendingHint" = "Discard the answer retained because the cursor or input field changed.";
|
||||
"keyboard.ai.replyVariant.ordinary" = "Ordinary";
|
||||
"keyboard.ai.replyVariant.formal" = "Formal";
|
||||
"keyboard.ai.replyVariant.playful" = "Relaxed & playful";
|
||||
"keyboard.ai.replyVariant.insertHint" = "Insert this complete reply.";
|
||||
"keyboard.assistant.dismissClipboard" = "Dismiss clipboard suggestions";
|
||||
"keyboard.assistant.dismissClipboardHint" = "Hide the current clipboard summary and skills.";
|
||||
"keyboard.ai.error.missingAPIKey" = "Configure an AI service in the main app first";
|
||||
@@ -334,6 +338,7 @@
|
||||
"keyboard.ai.skill.acceptTask" = "Acknowledge";
|
||||
"keyboard.ai.skill.clarifyRequest" = "Clarify";
|
||||
"keyboard.ai.skill.empathyReply" = "Empathize";
|
||||
"keyboard.ai.skill.blessingReply" = "Reply to wishes";
|
||||
"keyboard.ai.skill.askForDetails" = "Ask details";
|
||||
"keyboard.ai.skill.businessReply" = "Business";
|
||||
"keyboard.ai.skill.organizeList" = "Organize";
|
||||
|
||||
@@ -311,6 +311,10 @@
|
||||
"keyboard.assistant.insertPending" = "插入保留的 AI 回答";
|
||||
"keyboard.assistant.discardPending" = "丢弃 AI 回答";
|
||||
"keyboard.assistant.discardPendingHint" = "丢弃因光标或输入框变化而保留的回答。";
|
||||
"keyboard.ai.replyVariant.ordinary" = "普通";
|
||||
"keyboard.ai.replyVariant.formal" = "正式";
|
||||
"keyboard.ai.replyVariant.playful" = "轻松趣味";
|
||||
"keyboard.ai.replyVariant.insertHint" = "插入这条完整回复。";
|
||||
"keyboard.assistant.dismissClipboard" = "关闭剪贴板建议";
|
||||
"keyboard.assistant.dismissClipboardHint" = "隐藏当前剪贴板摘要和技能。";
|
||||
"keyboard.ai.error.missingAPIKey" = "请先在主 App 配置可用的 AI 服务";
|
||||
@@ -334,6 +338,7 @@
|
||||
"keyboard.ai.skill.acceptTask" = "确认任务";
|
||||
"keyboard.ai.skill.clarifyRequest" = "澄清追问";
|
||||
"keyboard.ai.skill.empathyReply" = "共情回复";
|
||||
"keyboard.ai.skill.blessingReply" = "回复祝福";
|
||||
"keyboard.ai.skill.askForDetails" = "询问细节";
|
||||
"keyboard.ai.skill.businessReply" = "商务回复";
|
||||
"keyboard.ai.skill.organizeList" = "整理清单";
|
||||
|
||||
Reference in New Issue
Block a user