feat(keyboard): unify assistant voice and AI workflows

Merge dictation and AI controls into one assistant surface, preserve safe insertion and clipboard actions, and align settings and tests with the new flow.
This commit is contained in:
Rocky
2026-08-16 12:07:06 +08:00
parent 46f6d818b8
commit fb97ec2937
45 changed files with 1969 additions and 2143 deletions
+2
View File
@@ -39,6 +39,8 @@ struct OSGKeyboardApp: App {
AIKeyboardDemoView()
} else if ProcessInfo.processInfo.arguments.contains("--ai-skills-demo") {
AIClipboardSkillLayoutDemoView()
} else if ProcessInfo.processInfo.arguments.contains("--assistant-ui-test") {
AssistantKeyboardUITestHarness()
} else if ProcessInfo.processInfo.arguments.contains("--clipboard-demo") {
ClipboardHistoryDemoView()
} else if ProcessInfo.processInfo.arguments.contains("--edit-pager-ui-test") {
+22 -3
View File
@@ -22,6 +22,14 @@ enum AppPermissions {
case restricted
}
enum PasteAccessResult: Equatable {
case verified
case noTextAvailable
case unavailable
}
private static let pasteAccessVerifiedKey = "clipboard.pasteAccessVerified.v1"
static var micStatus: MicStatus {
switch AVAudioApplication.shared.recordPermission {
case .granted: return .granted
@@ -46,6 +54,12 @@ enum AppPermissions {
micStatus == .granted && speechStatus == .granted
}
/// iOS does not expose the current "Paste from Other Apps" setting.
/// This records the last explicit, successful user-initiated read instead.
static var hasVerifiedPasteAccess: Bool {
UserDefaults.standard.bool(forKey: pasteAccessVerifiedKey)
}
/// Show guided permission pages when any Flow permission is not granted.
static var needsPermissionGuidance: Bool {
micStatus != .granted || speechStatus != .granted
@@ -82,10 +96,15 @@ enum AppPermissions {
/// and create the app's "Paste from Other Apps" settings entry.
@MainActor
@discardableResult
static func requestPasteAccess() -> Bool {
static func requestPasteAccess() -> PasteAccessResult {
let pasteboard = UIPasteboard.general
guard pasteboard.hasStrings else { return false }
return pasteboard.string != nil
guard pasteboard.hasStrings else { return .noTextAvailable }
guard pasteboard.string != nil else {
UserDefaults.standard.set(false, forKey: pasteAccessVerifiedKey)
return .unavailable
}
UserDefaults.standard.set(true, forKey: pasteAccessVerifiedKey)
return .verified
}
/// Home-screen guidance when Flow permissions are missing after onboarding.
+172 -42
View File
@@ -12,13 +12,17 @@ import OSGKeyboardShared
struct AIAgentSkillsView: View {
@Environment(\.themePalette) private var palette
@Environment(\.scenePhase) private var scenePhase
@ObservedObject private var config = ProviderConfig.shared
@ObservedObject private var store = AIAgentSkillLayoutStore.shared
@State private var viewingSkill: AIClipboardSkill?
@State private var editingDraft: SkillEditorDraft?
@State private var showFullAlert = false
@State private var showClipboardSettings = false
@State private var pasteAccessVerified = AppPermissions.hasVerifiedPasteAccess
@State private var pasteAccessNeedsRecovery = false
@State private var showPasteNoTextAlert = false
@State private var showPasteAccessSuccess = false
/// True while a skill card is lifted; locks the page scroll like SpringBoard.
@State private var isReordering = false
@@ -35,8 +39,9 @@ struct AIAgentSkillsView: View {
NavigationStack {
ScrollView {
CardPageContent(spacing: Spacing.xl) {
if !config.clipboardHistoryEnabled {
clipboardHistoryBanner
if showsClipboardAccessGuide {
clipboardAccessGuide
.transition(.opacity.combined(with: .move(edge: .top)))
}
enabledSection
if !store.availableSkills.isEmpty {
@@ -59,9 +64,6 @@ struct AIAgentSkillsView: View {
.accessibilityLabel(Text("skills.add"))
}
}
.navigationDestination(isPresented: $showClipboardSettings) {
ClipboardSettingsView(config: config)
}
}
.sheet(item: $viewingSkill) { skill in
SkillDetailSheet(
@@ -90,53 +92,62 @@ struct AIAgentSkillsView: View {
} message: {
Text(AppL10n.string("skills.full.message", language: config.uiLanguage))
}
.onAppear { store.reload() }
.alert(
AppL10n.string("clipboard.paste.noText.title", language: config.uiLanguage),
isPresented: $showPasteNoTextAlert
) {
Button("common.done") { showPasteNoTextAlert = false }
} message: {
Text(AppL10n.string("clipboard.paste.noText.message", language: config.uiLanguage))
}
.onAppear {
store.reload()
refreshPasteAccessState()
}
.onChange(of: scenePhase) { _, phase in
guard phase == .active else { return }
refreshPasteAccessState()
pasteAccessNeedsRecovery = false
}
}
private var clipboardHistoryBanner: some View {
private var clipboardAccessGuide: some View {
VStack(alignment: .leading, spacing: Spacing.sm) {
Label {
Text("skills.clipboard.guide.title")
.font(TypeStyle.bodyEmph)
.foregroundStyle(palette.textPrimary)
} icon: {
Image(systemName: "clipboard")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(palette.warning)
}
Text("skills.clipboard.guide.body")
.font(TypeStyle.caption)
.foregroundStyle(palette.textSecondary)
.fixedSize(horizontal: false, vertical: true)
HStack(alignment: .top, spacing: Spacing.sm) {
Image(systemName: clipboardGuideIcon)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(clipboardGuideTint)
.frame(width: 36, height: 36)
.background(clipboardGuideTint.opacity(0.12), in: Circle())
Button {
showClipboardSettings = true
} label: {
guideRow(
titleKey: "skills.clipboard.guide.openAppSettings",
systemImage: "slider.horizontal.3",
trailing: "chevron.right"
)
VStack(alignment: .leading, spacing: 4) {
Text(clipboardGuideTitle)
.font(TypeStyle.bodyEmph)
.foregroundStyle(palette.textPrimary)
Text(clipboardGuideBody)
.font(TypeStyle.caption)
.foregroundStyle(palette.textSecondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.buttonStyle(.plain)
Divider().background(palette.divider)
Button {
AppPermissions.openSystemSettings()
} label: {
guideRow(
titleKey: "skills.clipboard.guide.openSystemSettings",
systemImage: "gearshape",
trailing: "arrow.up.right"
)
if !showPasteAccessSuccess {
Button(action: performClipboardGuideAction) {
guideRow(
titleKey: clipboardGuideActionTitle,
systemImage: clipboardGuideActionIcon,
trailing: clipboardGuideActionTrailing
)
}
.buttonStyle(.plain)
.accessibilityIdentifier(clipboardGuideActionIdentifier)
}
.buttonStyle(.plain)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(Spacing.md)
.background(palette.surface, in: skillCardShape)
.overlay(skillCardShape.stroke(palette.divider, lineWidth: 0.5))
.accessibilityIdentifier("skills.clipboard.guide")
}
private func guideRow(
@@ -157,10 +168,129 @@ struct AIAgentSkillsView: View {
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(palette.textTertiary)
}
.padding(.vertical, Spacing.xs)
.padding(.horizontal, Spacing.sm)
.frame(minHeight: 44)
.background(
palette.surfaceElevated,
in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous)
)
.contentShape(Rectangle())
}
private var showsClipboardAccessGuide: Bool {
!config.clipboardHistoryEnabled || !pasteAccessVerified || showPasteAccessSuccess
}
private var clipboardGuideTitle: LocalizedStringKey {
if showPasteAccessSuccess {
return "skills.clipboard.guide.success.title"
}
if !config.clipboardHistoryEnabled {
return "skills.clipboard.guide.title"
}
if pasteAccessNeedsRecovery {
return "skills.clipboard.guide.recovery.title"
}
return "skills.clipboard.guide.verify.title"
}
private var clipboardGuideBody: LocalizedStringKey {
if showPasteAccessSuccess {
return "skills.clipboard.guide.success.body"
}
if !config.clipboardHistoryEnabled {
return "skills.clipboard.guide.body"
}
if pasteAccessNeedsRecovery {
return "skills.clipboard.guide.recovery.body"
}
return "skills.clipboard.guide.verify.body"
}
private var clipboardGuideIcon: String {
if showPasteAccessSuccess {
return "checkmark"
}
return pasteAccessNeedsRecovery ? "exclamationmark" : "clipboard"
}
private var clipboardGuideTint: Color {
pasteAccessNeedsRecovery ? palette.warning : palette.accent
}
private var clipboardGuideActionTitle: LocalizedStringKey {
if !config.clipboardHistoryEnabled {
return "skills.clipboard.guide.enableHistory"
}
if pasteAccessNeedsRecovery {
return "skills.clipboard.guide.openSystemSettings"
}
return "skills.clipboard.guide.verify.action"
}
private var clipboardGuideActionIcon: String {
if !config.clipboardHistoryEnabled {
return "clock.arrow.circlepath"
}
return pasteAccessNeedsRecovery ? "gearshape" : "checkmark.shield"
}
private var clipboardGuideActionTrailing: String {
pasteAccessNeedsRecovery ? "arrow.up.right" : "arrow.right"
}
private var clipboardGuideActionIdentifier: String {
if !config.clipboardHistoryEnabled {
return "skills.clipboard.guide.enableHistory"
}
if pasteAccessNeedsRecovery {
return "skills.clipboard.guide.openSystemSettings"
}
return "skills.clipboard.guide.verifyPaste"
}
private func performClipboardGuideAction() {
if !config.clipboardHistoryEnabled {
withAnimation(Motion.soft) {
config.clipboardHistoryEnabled = true
}
return
}
if pasteAccessNeedsRecovery {
AppPermissions.openSystemSettings()
return
}
verifyPasteAccess()
}
private func verifyPasteAccess() {
switch AppPermissions.requestPasteAccess() {
case .verified:
withAnimation(Motion.soft) {
pasteAccessVerified = true
pasteAccessNeedsRecovery = false
showPasteAccessSuccess = true
}
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(700))
withAnimation(Motion.soft) {
showPasteAccessSuccess = false
}
}
case .noTextAvailable:
showPasteNoTextAlert = true
case .unavailable:
withAnimation(Motion.soft) {
pasteAccessVerified = false
pasteAccessNeedsRecovery = true
}
}
}
private func refreshPasteAccessState() {
pasteAccessVerified = AppPermissions.hasVerifiedPasteAccess
}
private var enabledSection: some View {
CardSection(
title: AppL10n.format(
@@ -0,0 +1,189 @@
#if DEBUG
import SwiftUI
import UIKit
import OSGKeyboardShared
/// Deterministic host for simulator UI tests of the real assistant keyboard.
/// It exercises gesture routing and closed UI states without requiring a
/// keyboard-extension process, microphone permission, ASR, or an LLM.
struct AssistantKeyboardUITestHarness: View {
private enum Scenario: String {
case idle
case completed
case pending
case skillFailure
case skills
}
@StateObject private var state = KeyboardState()
@StateObject private var typing = TypingSessionController()
@State private var configured = false
@Environment(\.colorScheme) private var colorScheme
init() {
AIKeyboardView.debugSkipsLongPressCoach = true
AIKeyboardView.debugKeepsSkillTip = true
}
private let scenario: Scenario = {
let prefix = "--assistant-state="
let raw = ProcessInfo.processInfo.arguments
.first(where: { $0.hasPrefix(prefix) })?
.dropFirst(prefix.count)
return raw.flatMap { Scenario(rawValue: String($0)) } ?? .idle
}()
var body: some View {
GeometryReader { proxy in
VStack(spacing: 0) {
Spacer(minLength: 0)
AIKeyboardView(
state: state,
typing: typing,
onInsert: { _ in }
)
.background(backgroundColor)
}
.onAppear {
configure(width: proxy.size.width)
}
.onChange(of: proxy.size.width) { _, width in
configureLayout(width: width)
}
}
.background(backgroundColor.ignoresSafeArea())
.onDisappear {
AIKeyboardView.debugPreviewSkills = nil
AIKeyboardView.debugSkipsLongPressCoach = false
AIKeyboardView.debugKeepsSkillTip = false
}
}
private var backgroundColor: Color {
colorScheme == .dark ? Palette.dark.background : Palette.light.background
}
private func configure(width: CGFloat) {
configureLayout(width: width)
guard !configured else { return }
configured = true
state.surface = .voice
state.phase = .idle
state.aiServiceAvailable = true
state.micDisabled = false
state.returnKeyRole = .send
let keyboardState = state
state.tapMic = { [weak keyboardState] in
guard let keyboardState else { return }
if case .recording = keyboardState.phase {
keyboardState.phase = .idle
} else {
keyboardState.phase = .recording
}
}
state.tapAIMic = { [weak keyboardState] in
guard let keyboardState else { return }
if keyboardState.aiSession.phase == .listening {
if let utteranceID = keyboardState.aiSession.activeUtteranceID {
keyboardState.aiSession.beginRecognizing(utteranceID: utteranceID)
}
return
}
keyboardState.aiSession.enter()
let utteranceID = UUID()
keyboardState.aiSession.beginPreparing(utteranceID: utteranceID)
keyboardState.aiSession.beginListening(utteranceID: utteranceID)
}
state.sendAssistantAction = { [weak keyboardState] in
keyboardState?.assistantSendAvailable = false
}
state.undoLastInsertion = { [weak keyboardState] in
keyboardState?.undoAvailable = false
}
state.beginEditLastInput = { [weak keyboardState] in
guard let keyboardState else { return }
let reference = EditableInputReference(
displayText: "Original dictated input",
insertedText: "Original dictated input",
postInsertionFingerprint: nil,
extensionInstanceID: UUID()
)
keyboardState.editSession = .listening(EditSessionSource(reference: reference))
keyboardState.phase = .recording
}
state.stopEditListening = { [weak keyboardState] in
guard let keyboardState,
let source = keyboardState.editSession.source else {
return
}
keyboardState.editSession = .review(
EditReview(
source: source,
resultText: "Edited dictated input",
utteranceID: UUID()
)
)
keyboardState.phase = .processing
}
state.confirmEditResult = { [weak keyboardState] in
keyboardState?.editSession = .inactive
keyboardState?.phase = .idle
}
state.submitAIHint = { [weak keyboardState] _ in
guard let keyboardState else { return }
let utteranceID = UUID()
keyboardState.aiSession.enter()
keyboardState.aiSession.beginPreparing(utteranceID: utteranceID)
keyboardState.aiSession.beginGenerating(
question: "Deterministic hint",
utteranceID: utteranceID
)
}
switch scenario {
case .idle:
AIKeyboardView.debugPreviewSkills = nil
case .completed:
AIKeyboardView.debugPreviewSkills = nil
state.undoAvailable = true
state.editAvailable = true
state.assistantSendAvailable = true
case .pending:
AIKeyboardView.debugPreviewSkills = nil
let utteranceID = UUID()
state.aiSession.enter()
state.aiSession.beginPreparing(utteranceID: utteranceID)
state.aiSession.receiveAnswer(
"A retained answer that requires explicit insertion.",
utteranceID: utteranceID
)
state.confirmPendingAIAnswer = { [weak keyboardState] in
guard let keyboardState else { return }
keyboardState.aiSession.markAnswerInserted(offersSend: true)
keyboardState.undoAvailable = true
keyboardState.editAvailable = true
keyboardState.assistantSendAvailable = true
}
state.discardPendingAIAnswer = { [weak keyboardState] in
keyboardState?.aiSession.discardReadyAnswer()
}
case .skillFailure:
AIKeyboardView.debugPreviewSkills = nil
state.skillTipText = "Skill failed"
case .skills:
AIKeyboardView.debugPreviewSkills = AIClipboardSkillCatalog.catalog
state.undoAvailable = true
state.editAvailable = true
}
}
private func configureLayout(width: CGFloat) {
let isIPad = UIDevice.current.userInterfaceIdiom == .pad
state.layoutWidth = width
state.usesIPadLayoutMetrics = isIPad
state.showsSystemGlobeKey = isIPad
}
}
#endif
@@ -232,23 +232,6 @@ struct RememberLastSurfaceToggleRow: View {
}
}
// MARK: - Cursor drag navigation toggle
struct CursorDragNavigationToggleRow: View {
@Environment(\.themePalette) private var palette: ThemePalette
@Binding var isOn: Bool
var body: some View {
Toggle(isOn: $isOn) {
Text("settings.cursorDragNavigation.title")
.font(TypeStyle.body)
.foregroundStyle(palette.textPrimary)
}
.tint(palette.accent)
.settingsListRow()
}
}
// MARK: - Menu picker row (generic)
struct SettingsMenuPickerRow: View {
+85 -32
View File
@@ -259,10 +259,6 @@ struct GeneralSettingsView: View {
set: { config.keyboardHapticIntensity = $0 }
)
)
Divider().background(palette.divider)
CursorDragNavigationToggleRow(
isOn: $config.cursorDragNavigationEnabled
)
}
.surfaceCard()
}
@@ -310,9 +306,13 @@ struct AIAgentSettingsView: View {
struct ClipboardSettingsView: View {
@Environment(\.themePalette) private var palette: ThemePalette
@Environment(\.scenePhase) private var scenePhase
@ObservedObject var config: ProviderConfig
@ObservedObject private var history = ClipboardHistoryStore.shared
@State private var showClearConfirmation = false
@State private var pasteAccessVerified = AppPermissions.hasVerifiedPasteAccess
@State private var pasteAccessNeedsRecovery = false
@State private var showPasteNoTextAlert = false
var body: some View {
ScrollView {
@@ -356,40 +356,58 @@ struct ClipboardSettingsView: View {
// "Paste from Other Apps" permission to Allow.
CardSection("settings.clipboard.paste.section") {
VStack(spacing: 0) {
Text("settings.clipboard.paste.body")
.font(.footnote)
.foregroundStyle(palette.textSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
.padding(.vertical, 12)
Divider().background(palette.divider)
Button {
AppPermissions.requestPasteAccess()
} label: {
SettingsNavigationRow(
titleText: AppL10n.string(
"settings.clipboard.paste.request",
language: config.uiLanguage
HStack(alignment: .center, spacing: Spacing.sm) {
Text("settings.clipboard.paste.body")
.font(.footnote)
.foregroundStyle(palette.textSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
if pasteAccessVerified {
Label(
"settings.clipboard.paste.verified",
systemImage: "checkmark.circle.fill"
)
)
.font(TypeStyle.caption)
.foregroundStyle(palette.accent)
.fixedSize()
.accessibilityIdentifier("settings.clipboard.paste.verified")
}
}
.buttonStyle(.plain)
.padding(.horizontal, 16)
.padding(.vertical, 12)
Divider().background(palette.divider)
if !pasteAccessVerified {
Divider().background(palette.divider)
Button {
AppPermissions.openSystemSettings()
} label: {
SettingsNavigationRow(
titleText: AppL10n.string(
"settings.clipboard.paste.open",
language: config.uiLanguage
Button {
verifyPasteAccess()
} label: {
SettingsNavigationRow(
titleText: AppL10n.string(
"settings.clipboard.paste.request",
language: config.uiLanguage
)
)
)
}
.buttonStyle(.plain)
.accessibilityIdentifier("settings.clipboard.paste.verify")
if pasteAccessNeedsRecovery {
Divider().background(palette.divider)
Button {
AppPermissions.openSystemSettings()
} label: {
SettingsNavigationRow(
titleText: AppL10n.string(
"settings.clipboard.paste.open",
language: config.uiLanguage
)
)
}
.buttonStyle(.plain)
.accessibilityIdentifier("settings.clipboard.paste.openSettings")
}
}
.buttonStyle(.plain)
}
.surfaceCard()
}
@@ -441,8 +459,22 @@ struct ClipboardSettingsView: View {
} message: {
Text("settings.clipboard.clear.message")
}
.alert(
AppL10n.string("clipboard.paste.noText.title", language: config.uiLanguage),
isPresented: $showPasteNoTextAlert
) {
Button("common.done") { showPasteNoTextAlert = false }
} message: {
Text(AppL10n.string("clipboard.paste.noText.message", language: config.uiLanguage))
}
.onAppear {
history.reload()
refreshPasteAccessState()
}
.onChange(of: scenePhase) { _, phase in
guard phase == .active else { return }
refreshPasteAccessState()
pasteAccessNeedsRecovery = false
}
.onChange(of: config.clipboardHistoryEnabled) { _, enabled in
if !enabled {
@@ -457,6 +489,27 @@ struct ClipboardSettingsView: View {
set: { config.clipboardCandidateBarEnabled = $0 }
)
}
private func verifyPasteAccess() {
switch AppPermissions.requestPasteAccess() {
case .verified:
withAnimation(Motion.quick) {
pasteAccessVerified = true
pasteAccessNeedsRecovery = false
}
case .noTextAvailable:
showPasteNoTextAlert = true
case .unavailable:
withAnimation(Motion.quick) {
pasteAccessVerified = false
pasteAccessNeedsRecovery = true
}
}
}
private func refreshPasteAccessState() {
pasteAccessVerified = AppPermissions.hasVerifiedPasteAccess
}
}
// MARK: - About
+18 -8
View File
@@ -205,15 +205,18 @@
"settings.clipboard.subtitle.on" = "On";
"settings.clipboard.subtitle.off" = "Off";
"settings.clipboard.history.title" = "History";
"settings.clipboard.history.footer" = "Off by default. Captures text copied on this device or through Universal Clipboard and keeps up to 15 items in this devices App Group. AI mode can suggest clipboard-related prompts for about 30 seconds after a copy.";
"settings.clipboard.history.footer" = "Keeps up to 15 copied text items on this device. Off by default.";
"settings.clipboard.candidate.title" = "Suggestion strip";
"settings.clipboard.candidate.footer" = "Shows the newest copy above the keys for one-tap insert.";
"settings.clipboard.candidate.footer" = "Shows your latest copy above the keyboard. Tap to insert.";
"settings.clipboard.paste.section" = "System access";
"settings.clipboard.paste.body" = "Copy some text, tap Request Paste Access, and allow access. iOS will then create the Paste from Other Apps setting, where you can select Allow.";
"settings.clipboard.paste.request" = "Request Paste Access";
"settings.clipboard.paste.body" = "Allows clipboard skills to read text you copy.";
"settings.clipboard.paste.request" = "Verify Paste Access";
"settings.clipboard.paste.verified" = "Verified";
"settings.clipboard.paste.open" = "Open iOS Settings";
"settings.clipboard.storage.section" = "Local history";
"settings.clipboard.storage.body" = "Turning History off stops capture and the suggestion strip but keeps saved items. Clipboard history does not sync through iCloud and is never sent to AI automatically. After you insert it, active polish may include it as context for your configured provider. Sensitive filtering is conservative and cannot detect every password.";
"settings.clipboard.storage.body" = "History stays on this device, does not sync through iCloud, and is never sent to AI automatically. Turning History off keeps saved items.";
"clipboard.paste.noText.title" = "Copy text first";
"clipboard.paste.noText.message" = "Copy a short piece of text, then try again.";
"settings.clipboard.clear.button" = "Clear clipboard history";
"settings.clipboard.clear.title" = "Clear clipboard history?";
"settings.clipboard.clear.message" = "This permanently removes all saved clipboard items from this device. This cannot be undone.";
@@ -440,9 +443,16 @@
"skills.enabled.section" = "In use (%d/%d)";
"skills.enabled.empty" = "No skills on the keyboard. Turn one on from the list below.";
"skills.available.section" = "Available";
"skills.clipboard.guide.title" = "Clipboard access needed";
"skills.clipboard.guide.body" = "Turn on Clipboard History in the app, then allow paste access in iOS Settings, so these skills can run after you copy text.";
"skills.clipboard.guide.openAppSettings" = "Open Clipboard settings";
"skills.clipboard.guide.title" = "Enable clipboard skills";
"skills.clipboard.guide.body" = "Copy text to summarize, translate, or extract actions from it.";
"skills.clipboard.guide.enableHistory" = "Turn On Clipboard History";
"skills.clipboard.guide.verify.title" = "One more step";
"skills.clipboard.guide.verify.body" = "Allow OSGKeyboard to read text you copy.";
"skills.clipboard.guide.verify.action" = "Verify Paste Access";
"skills.clipboard.guide.recovery.title" = "Paste access is off";
"skills.clipboard.guide.recovery.body" = "Allow Paste from Other Apps in iOS Settings, then return to verify.";
"skills.clipboard.guide.success.title" = "Clipboard skills are ready";
"skills.clipboard.guide.success.body" = "Copy text to use them from the keyboard.";
"skills.clipboard.guide.openSystemSettings" = "Open iOS Settings";
"skills.detail.title" = "Skill";
"skills.badge.default" = "Default skill";
+18 -8
View File
@@ -205,15 +205,18 @@
"settings.clipboard.subtitle.on" = "已开启";
"settings.clipboard.subtitle.off" = "已关闭";
"settings.clipboard.history.title" = "历史记录";
"settings.clipboard.history.footer" = "默认关闭。开启后采集本机或通用剪贴板复制的文本,并在本机 App Group 中保存最近 15 条复制约 30 秒内,AI 模式也可展示剪贴板相关建议。";
"settings.clipboard.history.footer" = "在本机保存最近 15 条复制文字,默认关闭。";
"settings.clipboard.candidate.title" = "建议条";
"settings.clipboard.candidate.footer" = "最新复制显示在键盘上方,点一下即可插入。";
"settings.clipboard.candidate.footer" = "在键盘上方显示最新复制,轻点即可插入。";
"settings.clipboard.paste.section" = "系统授权";
"settings.clipboard.paste.body" = "先复制一段文字,再点「请求粘贴权限」并允许访问;iOS 随后会生成「从其他 App 粘贴」设置项,可将其设为「允许」。";
"settings.clipboard.paste.request" = "请求粘贴权限";
"settings.clipboard.paste.body" = "允许剪贴板技能读取你主动复制的文字。";
"settings.clipboard.paste.request" = "验证粘贴访问";
"settings.clipboard.paste.verified" = "已验证";
"settings.clipboard.paste.open" = "打开系统设置";
"settings.clipboard.storage.section" = "本机历史";
"settings.clipboard.storage.body" = "关闭「历史记录」只会停止采集并关闭建议条,已有记录仍会保留。剪贴板历史不经 iCloud 同步,也不会自动发送给 AI;插入后若主动使用润色,内容可能作为上下文发送给你配置的服务商。敏感内容过滤采用保守规则,无法识别所有密码。";
"settings.clipboard.storage.body" = "历史仅保存在本机,不经 iCloud 同步,也不会自动发送给 AI。关闭历史不会删除已有记录。";
"clipboard.paste.noText.title" = "请先复制文字";
"clipboard.paste.noText.message" = "复制一小段文字后,再试一次。";
"settings.clipboard.clear.button" = "清空剪贴板历史";
"settings.clipboard.clear.title" = "清空剪贴板历史?";
"settings.clipboard.clear.message" = "将从本机永久删除全部剪贴板历史,且无法撤销。";
@@ -439,9 +442,16 @@
"skills.enabled.section" = "使用中(%d/%d";
"skills.enabled.empty" = "键盘上还没有技能。从下方列表打开一个即可。";
"skills.available.section" = "可添加";
"skills.clipboard.guide.title" = "需要剪贴板权限";
"skills.clipboard.guide.body" = "请先在 App 里打开剪贴板历史,再到系统设置中允许粘贴授权,复制后才能使用这些技能。";
"skills.clipboard.guide.openAppSettings" = "打开剪贴板设置";
"skills.clipboard.guide.title" = "启用剪贴板技能";
"skills.clipboard.guide.body" = "复制文字后,可直接总结、翻译或提取事项。";
"skills.clipboard.guide.enableHistory" = "开启历史记录";
"skills.clipboard.guide.verify.title" = "还差一步";
"skills.clipboard.guide.verify.body" = "允许 OSGKeyboard 读取你主动复制的文字。";
"skills.clipboard.guide.verify.action" = "验证粘贴访问";
"skills.clipboard.guide.recovery.title" = "粘贴访问未开启";
"skills.clipboard.guide.recovery.body" = "在系统设置中允许「从其他 App 粘贴」,返回后再验证。";
"skills.clipboard.guide.success.title" = "剪贴板技能已就绪";
"skills.clipboard.guide.success.body" = "复制文字后,即可从键盘使用这些技能。";
"skills.clipboard.guide.openSystemSettings" = "打开系统设置";
"skills.detail.title" = "技能";
"skills.badge.default" = "默认技能";