feat(keyboard): add Skills tab and extract-todos Shortcut export
Ship a Skills catalog with drag-to-reorder chips and a companion Shortcut that receives extracted titles and writes them to Reminders.
This commit is contained in:
@@ -69,6 +69,7 @@
|
||||
<string>zalo</string>
|
||||
<string>skype</string>
|
||||
<string>zoomus</string>
|
||||
<string>shortcuts</string>
|
||||
<string>things</string>
|
||||
<string>todoist</string>
|
||||
<string>evernote</string>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,41 @@
|
||||
// AIAgentShortcutInstaller.swift
|
||||
// OSGKeyboard · Main App
|
||||
//
|
||||
// Opening the HTTPS iCloud share page from the app is claimed as a
|
||||
// Universal Link and often lands on Gallery. `shortcuts://shortcuts/TOKEN`
|
||||
// opens the Add sheet in Shortcuts directly.
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import OSGKeyboardShared
|
||||
|
||||
enum AIAgentShortcutInstaller {
|
||||
static let bundledResourceName = "OSGExtractTodos"
|
||||
|
||||
@MainActor
|
||||
static func openInstallPage(for skill: AIClipboardSkill) {
|
||||
if let shareURL = skill.shortcutICloudURL,
|
||||
let installURL = AIAgentShortcutRun.shortcutsInstallURL(from: shareURL) {
|
||||
UIApplication.shared.open(installURL)
|
||||
return
|
||||
}
|
||||
openBundledShortcut()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func openBundledShortcut() {
|
||||
guard let bundled = Bundle.main.url(
|
||||
forResource: bundledResourceName,
|
||||
withExtension: "shortcut"
|
||||
) else { return }
|
||||
let tmp = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("\(AIClipboardSkillCatalog.extractTodosShortcutName).shortcut")
|
||||
try? FileManager.default.removeItem(at: tmp)
|
||||
do {
|
||||
try FileManager.default.copyItem(at: bundled, to: tmp)
|
||||
UIApplication.shared.open(tmp)
|
||||
} catch {
|
||||
UIApplication.shared.open(bundled)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// AIAgentShortcutRunner.swift
|
||||
// OSGKeyboard · Main App
|
||||
//
|
||||
// Consumes the keyboard's pending extract-todos payload and opens the
|
||||
// companion Shortcut. Release builds stay in Shortcuts. DEBUG builds add
|
||||
// x-callback URLs so Console can record success / error / cancel.
|
||||
|
||||
import UIKit
|
||||
import OSGKeyboardShared
|
||||
|
||||
enum AIAgentShortcutRunner {
|
||||
@MainActor
|
||||
static func runPendingIfNeeded() {
|
||||
AIAgentShortcutRun.trace("host.runPending begin")
|
||||
guard let payload = AppGroupStore().consumePendingShortcutRun() else { return }
|
||||
guard let skill = AIClipboardSkillCatalog.skill(id: payload.skillID),
|
||||
let name = skill.shortcutName else {
|
||||
AIAgentShortcutRun.trace(
|
||||
"host.runPending skip unknownSkill=\(payload.skillID)"
|
||||
)
|
||||
return
|
||||
}
|
||||
AIAgentShortcutRun.traceBody("host.titlesToShortcut", payload.joinedTitles)
|
||||
guard let url = shortcutsURL(name: name, text: payload.joinedTitles) else {
|
||||
AIAgentShortcutRun.trace("host.runPending skip URLBuildFailed name=\(name)")
|
||||
return
|
||||
}
|
||||
let textItem = URLComponents(url: url, resolvingAgainstBaseURL: false)?
|
||||
.queryItems?
|
||||
.first { $0.name == "text" }?
|
||||
.value
|
||||
AIAgentShortcutRun.trace(
|
||||
"host.openShortcuts scheme=\(url.scheme ?? "") host=\(url.host ?? "") "
|
||||
+ "path=\(url.path) urlChars=\(url.absoluteString.count) "
|
||||
+ "textQueryChars=\(textItem?.count ?? -1) name=\(name)"
|
||||
)
|
||||
AIAgentShortcutRun.traceBody("host.textQuery", textItem ?? "")
|
||||
UIApplication.shared.open(url) { success in
|
||||
AIAgentShortcutRun.trace(
|
||||
"host.openShortcuts result success=\(success) "
|
||||
+ "(iOS accepted the URL; not proof Reminders were created)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static func logShortcutCallback(_ url: URL) {
|
||||
let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
|
||||
let status = items.first { $0.name == "status" }?.value ?? "unknown"
|
||||
let errorMessage = items.first { $0.name == "errorMessage" }?.value
|
||||
let errorCode = items.first { $0.name == "errorCode" }?.value
|
||||
AIAgentShortcutRun.trace(
|
||||
"host.shortcutCallback status=\(status) errorCode=\(errorCode ?? "-") "
|
||||
+ "errorMessage=\(errorMessage ?? "-") "
|
||||
+ "(Shortcuts finished; does not prove reminder rows exist)"
|
||||
)
|
||||
}
|
||||
|
||||
private static func shortcutsURL(name: String, text: String) -> URL? {
|
||||
#if DEBUG
|
||||
return AIAgentShortcutRun.shortcutsRunURL(
|
||||
name: name,
|
||||
text: text,
|
||||
xSuccess: "osgkeyboard://skill/shortcut-result?status=success",
|
||||
xError: "osgkeyboard://skill/shortcut-result?status=error",
|
||||
xCancel: "osgkeyboard://skill/shortcut-result?status=cancel"
|
||||
)
|
||||
#else
|
||||
return AIAgentShortcutRun.shortcutsRunURL(name: name, text: text)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// AIAgentSkillsView.swift
|
||||
// OSGKeyboard · Main App
|
||||
//
|
||||
// Catalog of AI Agent clipboard skills. Enabled cards (max 8) appear on
|
||||
// the keyboard after a copy; long-press drag reorders that row. Export
|
||||
// skills confirm a companion Shortcut before they can occupy a slot.
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import OSGKeyboardShared
|
||||
|
||||
struct AIAgentSkillsView: View {
|
||||
@Environment(\.themePalette) private var palette
|
||||
@ObservedObject private var config = ProviderConfig.shared
|
||||
@ObservedObject private var store = AIAgentSkillLayoutStore.shared
|
||||
|
||||
@State private var viewingSkill: AIClipboardSkill?
|
||||
@State private var showFullAlert = false
|
||||
|
||||
private let columns = [
|
||||
GridItem(.flexible(), spacing: Spacing.sm),
|
||||
GridItem(.flexible(), spacing: Spacing.sm),
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
CardPageContent(spacing: Spacing.xl) {
|
||||
if !config.clipboardHistoryEnabled {
|
||||
clipboardHistoryBanner
|
||||
}
|
||||
enabledSection
|
||||
if !store.availableSkills.isEmpty {
|
||||
availableSection
|
||||
}
|
||||
}
|
||||
.tabBarScrollBottomPadding()
|
||||
}
|
||||
.background(palette.background)
|
||||
.navigationTitle("skills.title")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
.sheet(item: $viewingSkill) { skill in
|
||||
SkillDetailSheet(
|
||||
store: store,
|
||||
skill: skill,
|
||||
onDismiss: { viewingSkill = nil },
|
||||
onAddOrWarn: addOrWarn,
|
||||
onConfirmInstall: confirmShortcutInstall
|
||||
)
|
||||
}
|
||||
.alert(
|
||||
AppL10n.string("skills.full.title", language: config.uiLanguage),
|
||||
isPresented: $showFullAlert
|
||||
) {
|
||||
Button("common.done") { showFullAlert = false }
|
||||
} message: {
|
||||
Text(AppL10n.string("skills.full.message", language: config.uiLanguage))
|
||||
}
|
||||
.onAppear { store.reload() }
|
||||
}
|
||||
|
||||
private var clipboardHistoryBanner: some View {
|
||||
Text("skills.clipboardHistory.banner")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(Spacing.md)
|
||||
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
|
||||
.stroke(palette.divider, lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
|
||||
private var enabledSection: some View {
|
||||
CardSection(
|
||||
title: AppL10n.format(
|
||||
"skills.enabled.section",
|
||||
language: config.uiLanguage,
|
||||
store.enabledSkills.count,
|
||||
AIAgentSkillLayout.maximumEnabled
|
||||
)
|
||||
) {
|
||||
if store.enabledSkills.isEmpty {
|
||||
Text("skills.enabled.empty")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.textTertiary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
} else {
|
||||
LazyVGrid(columns: columns, spacing: Spacing.sm) {
|
||||
ForEach(store.enabledSkills) { skill in
|
||||
skillCard(skill, showsEnabledBadge: true)
|
||||
.onTapGesture { viewingSkill = skill }
|
||||
.draggable(skill.id) {
|
||||
skillCard(skill, showsEnabledBadge: true)
|
||||
.frame(width: 160)
|
||||
}
|
||||
.dropDestination(for: String.self) { items, _ in
|
||||
guard let dragged = items.first else { return false }
|
||||
store.moveEnabled(id: dragged, onto: skill.id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var availableSection: some View {
|
||||
CardSection("skills.available.section") {
|
||||
LazyVGrid(columns: columns, spacing: Spacing.sm) {
|
||||
ForEach(store.availableSkills) { skill in
|
||||
Button {
|
||||
viewingSkill = skill
|
||||
} label: {
|
||||
skillCard(skill, showsEnabledBadge: false)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func skillCard(_ skill: AIClipboardSkill, showsEnabledBadge: Bool) -> some View {
|
||||
VStack(alignment: .leading, spacing: Spacing.sm) {
|
||||
HStack(spacing: Spacing.sm) {
|
||||
Image(systemName: skill.systemImage)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(palette.accent)
|
||||
Spacer(minLength: 0)
|
||||
if showsEnabledBadge {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundStyle(palette.accent)
|
||||
}
|
||||
}
|
||||
Text(AppL10n.string(skill.cardTitleKey, language: config.uiLanguage))
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
.lineLimit(1)
|
||||
Text(AppL10n.string(skill.descriptionKey, language: config.uiLanguage))
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(palette.textTertiary)
|
||||
.lineLimit(2)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.frame(maxWidth: .infinity, minHeight: 96, alignment: .leading)
|
||||
.padding(Spacing.md)
|
||||
.background(
|
||||
showsEnabledBadge ? palette.accentMuted : palette.surface,
|
||||
in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
|
||||
.stroke(
|
||||
showsEnabledBadge ? palette.accent : palette.divider,
|
||||
lineWidth: showsEnabledBadge ? 1.5 : 0.5
|
||||
)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
|
||||
.contentShape(RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
|
||||
}
|
||||
|
||||
private func addOrWarn(_ skill: AIClipboardSkill) {
|
||||
switch store.enable(skill.id) {
|
||||
case .enabled, .alreadyEnabled:
|
||||
viewingSkill = nil
|
||||
case .full:
|
||||
showFullAlert = true
|
||||
case .needsShortcut, .unknown:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func confirmShortcutInstall(_ skill: AIClipboardSkill) {
|
||||
switch store.confirmShortcutAndEnable(skill.id) {
|
||||
case .enabled, .alreadyEnabled:
|
||||
viewingSkill = nil
|
||||
case .full:
|
||||
showFullAlert = true
|
||||
case .needsShortcut, .unknown:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SkillDetailSheet: View {
|
||||
@Environment(\.themePalette) private var palette
|
||||
@ObservedObject var store: AIAgentSkillLayoutStore
|
||||
@ObservedObject private var config = ProviderConfig.shared
|
||||
|
||||
let skill: AIClipboardSkill
|
||||
let onDismiss: () -> Void
|
||||
let onAddOrWarn: (AIClipboardSkill) -> Void
|
||||
let onConfirmInstall: (AIClipboardSkill) -> Void
|
||||
|
||||
/// Content stack height; nav chrome is added for the detent.
|
||||
@State private var contentHeight: CGFloat = 240
|
||||
private let navigationChrome: CGFloat = 56
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
CardPageContent(
|
||||
spacing: Spacing.md,
|
||||
topPadding: Spacing.md,
|
||||
bottomPadding: Spacing.lg
|
||||
) {
|
||||
header
|
||||
explanation
|
||||
skillActions
|
||||
}
|
||||
.background(palette.background)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.onGeometryChange(for: CGFloat.self) { proxy in
|
||||
proxy.size.height
|
||||
} action: { newHeight in
|
||||
let next = newHeight + navigationChrome
|
||||
if abs(contentHeight - next) > 1 {
|
||||
contentHeight = next
|
||||
}
|
||||
}
|
||||
.navigationTitle("skills.detail.title")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("common.done", action: onDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.height(contentHeight)])
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationContentInteraction(.resizes)
|
||||
.animation(.easeInOut(duration: 0.2), value: contentHeight)
|
||||
}
|
||||
|
||||
private var explanation: some View {
|
||||
Text(AppL10n.string(explanationKey, language: config.uiLanguage))
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: .infinity, minHeight: 80, maxHeight: 80, alignment: .topLeading)
|
||||
}
|
||||
|
||||
private var explanationKey: String {
|
||||
if skill.requiresShortcut, !store.layout.hasConfirmedShortcut(skill.id) {
|
||||
return "skills.install.lead"
|
||||
}
|
||||
if skill.requiresShortcut, store.layout.isEnabled(skill.id) {
|
||||
return "skills.action.turnOffHint"
|
||||
}
|
||||
return skill.descriptionKey
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack(alignment: .center, spacing: Spacing.md) {
|
||||
Image(systemName: skill.systemImage)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(palette.accent)
|
||||
.frame(width: 36, height: 36)
|
||||
.background(palette.accentMuted, in: Circle())
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(AppL10n.string(skill.cardTitleKey, language: config.uiLanguage))
|
||||
.font(TypeStyle.title3)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
if skill.isDefault {
|
||||
Text("skills.badge.default")
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var skillActions: some View {
|
||||
let enabled = store.layout.isEnabled(skill.id)
|
||||
if skill.requiresShortcut {
|
||||
if !store.layout.hasConfirmedShortcut(skill.id) {
|
||||
shortcutInstallBlock
|
||||
} else {
|
||||
if enabled {
|
||||
fullWidthButton("skills.action.turnOff", prominent: false) {
|
||||
store.disable(skill.id)
|
||||
onDismiss()
|
||||
}
|
||||
} else {
|
||||
fullWidthButton("skills.action.addToKeyboard", prominent: true) {
|
||||
onAddOrWarn(skill)
|
||||
}
|
||||
}
|
||||
fullWidthButton("skills.action.reinstallShortcut", prominent: false) {
|
||||
openShortcutInstall()
|
||||
}
|
||||
}
|
||||
} else if enabled {
|
||||
fullWidthButton("skills.action.turnOff", prominent: false) {
|
||||
store.disable(skill.id)
|
||||
onDismiss()
|
||||
}
|
||||
} else {
|
||||
fullWidthButton("skills.action.addToKeyboard", prominent: true) {
|
||||
onAddOrWarn(skill)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var shortcutInstallBlock: some View {
|
||||
VStack(alignment: .leading, spacing: Spacing.sm) {
|
||||
fullWidthButton("skills.install.openShortcuts", prominent: true) {
|
||||
openShortcutInstall()
|
||||
}
|
||||
fullWidthButton("skills.install.confirmAdded", prominent: false) {
|
||||
onConfirmInstall(skill)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func fullWidthButton(
|
||||
_ titleKey: LocalizedStringKey,
|
||||
prominent: Bool,
|
||||
action: @escaping () -> Void
|
||||
) -> some View {
|
||||
let label = Text(titleKey)
|
||||
.frame(maxWidth: .infinity, minHeight: 38)
|
||||
if prominent {
|
||||
Button(action: action) { label }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(palette.accent)
|
||||
} else {
|
||||
Button(action: action) { label }
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
|
||||
private func openShortcutInstall() {
|
||||
AIAgentShortcutInstaller.openInstallPage(for: skill)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// MinimalTabBar.swift
|
||||
// OSGKeyboard · Main App
|
||||
//
|
||||
// Bottom tab bar — icon + label for Home / Styles / Settings.
|
||||
// Bottom tab bar — icon + label for Home / Skills / Styles / Settings.
|
||||
// The dock capsule is iOS 26 Liquid Glass; the selected tab is a green
|
||||
// fill inside that capsule (Photos-style), not a second glass layer.
|
||||
// History + dictionary live as Home cards (not dock tabs).
|
||||
@@ -11,13 +11,14 @@ import OSGKeyboardShared
|
||||
|
||||
enum AppTab: Int, CaseIterable {
|
||||
case keyboard
|
||||
case skills
|
||||
case styles
|
||||
case settings
|
||||
|
||||
var icon: MaterialIconName {
|
||||
switch self {
|
||||
case .keyboard: return .keyboard
|
||||
case .styles: return .menuBook // unused — styles uses SF Symbol
|
||||
case .skills, .styles: return .menuBook // unused — these tabs use SF Symbols
|
||||
case .settings: return .settings
|
||||
}
|
||||
}
|
||||
@@ -25,6 +26,7 @@ enum AppTab: Int, CaseIterable {
|
||||
/// SF Symbol overrides shared with the Mac and iPad sidebars.
|
||||
var sfSymbol: String? {
|
||||
switch self {
|
||||
case .skills: return "sparkles"
|
||||
case .styles: return "text.badge.star"
|
||||
default: return nil
|
||||
}
|
||||
@@ -33,6 +35,7 @@ enum AppTab: Int, CaseIterable {
|
||||
var accessibilityKey: LocalizedStringKey {
|
||||
switch self {
|
||||
case .keyboard: return "tab.keyboard"
|
||||
case .skills: return "tab.skills"
|
||||
case .styles: return "tab.styles"
|
||||
case .settings: return "tab.settings"
|
||||
}
|
||||
@@ -44,6 +47,7 @@ enum AppTab: Int, CaseIterable {
|
||||
var sidebarSystemImage: String {
|
||||
switch self {
|
||||
case .keyboard: return "house"
|
||||
case .skills: return "sparkles"
|
||||
case .styles: return "text.badge.star"
|
||||
case .settings: return "gearshape"
|
||||
}
|
||||
@@ -110,7 +114,7 @@ struct MinimalTabBar: View {
|
||||
.padding(.horizontal, TabBarDockMetrics.dockInsetHorizontal)
|
||||
.padding(.vertical, TabBarDockMetrics.dockInsetVertical)
|
||||
.glassEffect(.regular.interactive(), in: .capsule)
|
||||
.frame(maxWidth: 280)
|
||||
.frame(maxWidth: 360)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.bottom, TabBarDockMetrics.bottomPadding)
|
||||
}
|
||||
|
||||
@@ -214,6 +214,12 @@ struct MainAppRoot: View {
|
||||
switch url.host {
|
||||
case "startflow":
|
||||
flowManager.startSession(coldStart: true, reason: "url.startflow")
|
||||
case "skill":
|
||||
if url.path.contains("shortcut-result") {
|
||||
AIAgentShortcutRunner.logShortcutCallback(url)
|
||||
} else if url.path.contains("run") {
|
||||
AIAgentShortcutRunner.runPendingIfNeeded()
|
||||
}
|
||||
case "deployrime":
|
||||
// The keyboard sends the user here precisely because typing
|
||||
// resources are missing — deploy without waiting for warmup.
|
||||
|
||||
@@ -14,6 +14,8 @@ struct MainTabContent: View {
|
||||
switch tab {
|
||||
case .keyboard:
|
||||
HomeView()
|
||||
case .skills:
|
||||
AIAgentSkillsView()
|
||||
case .styles:
|
||||
PolishStylesView()
|
||||
case .settings:
|
||||
|
||||
@@ -427,8 +427,35 @@
|
||||
"tab.history" = "History";
|
||||
"tab.dictionary" = "Dictionary";
|
||||
"tab.styles" = "Styles";
|
||||
"tab.skills" = "Skills";
|
||||
"tab.settings" = "Settings";
|
||||
|
||||
/* AI Agent skills */
|
||||
"skills.title" = "Skills";
|
||||
"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.clipboardHistory.banner" = "Turn on Clipboard History in Settings to use these skills after you copy text.";
|
||||
"skills.detail.title" = "Skill";
|
||||
"skills.badge.default" = "Default skill";
|
||||
"skills.action.turnOff" = "Turn off";
|
||||
"skills.action.turnOffHint" = "Removes it from the keyboard only.";
|
||||
"skills.action.addToKeyboard" = "Add to keyboard";
|
||||
"skills.action.reinstallShortcut" = "Reinstall Shortcut";
|
||||
"skills.full.title" = "Skill slots full";
|
||||
"skills.full.message" = "You can enable up to 8 skills. Turn one off, then add this skill from its card.";
|
||||
"skills.reply.name" = "Reply";
|
||||
"skills.reply.description" = "Draft a polite reply from the copied text, ready to insert.";
|
||||
"skills.summarize.name" = "Summarize";
|
||||
"skills.summarize.description" = "Keep the key facts and conclusions, without rewriting it as a chat message.";
|
||||
"skills.translate.name" = "Translate";
|
||||
"skills.translate.description" = "Translate the copied text using your keyboard language setting.";
|
||||
"skills.extractTodos.name" = "Extract tasks";
|
||||
"skills.extractTodos.description" = "Extract to-dos from copied text into Reminders.";
|
||||
"skills.install.lead" = "Add it, then confirm. Don’t rename it.";
|
||||
"skills.install.openShortcuts" = "Add Shortcut";
|
||||
"skills.install.confirmAdded" = "I've added it";
|
||||
|
||||
/* Polish style packs */
|
||||
"polishStyles.title" = "Polish styles";
|
||||
"polishStyles.add" = "Add style";
|
||||
|
||||
@@ -426,8 +426,35 @@
|
||||
"tab.history" = "历史";
|
||||
"tab.dictionary" = "词库";
|
||||
"tab.styles" = "风格";
|
||||
"tab.skills" = "技能";
|
||||
"tab.settings" = "设置";
|
||||
|
||||
/* AI Agent 技能 */
|
||||
"skills.title" = "技能";
|
||||
"skills.enabled.section" = "使用中(%d/%d)";
|
||||
"skills.enabled.empty" = "键盘上还没有技能。从下方列表打开一个即可。";
|
||||
"skills.available.section" = "可添加";
|
||||
"skills.clipboardHistory.banner" = "请先在设置中打开剪贴板历史,复制文字后才能在键盘上使用这些技能。";
|
||||
"skills.detail.title" = "技能";
|
||||
"skills.badge.default" = "默认技能";
|
||||
"skills.action.turnOff" = "关闭";
|
||||
"skills.action.turnOffHint" = "只从键盘拿掉,不会删除捷径。";
|
||||
"skills.action.addToKeyboard" = "添加到键盘";
|
||||
"skills.action.reinstallShortcut" = "重新安装捷径";
|
||||
"skills.full.title" = "技能已满";
|
||||
"skills.full.message" = "最多同时启用 8 个技能。请先关闭一个,再从卡片手动添加。";
|
||||
"skills.reply.name" = "回复";
|
||||
"skills.reply.description" = "根据复制的内容起草一段礼貌回复,可插入当前输入框。";
|
||||
"skills.summarize.name" = "总结";
|
||||
"skills.summarize.description" = "保留关键事实与结论,不会改写成可发送的短消息。";
|
||||
"skills.translate.name" = "翻译";
|
||||
"skills.translate.description" = "按键盘目标语言翻译复制的内容。";
|
||||
"skills.extractTodos.name" = "提取待办";
|
||||
"skills.extractTodos.description" = "从复制内容提取待办,写入提醒事项。";
|
||||
"skills.install.lead" = "添加后点「我已添加」,请勿改名。";
|
||||
"skills.install.openShortcuts" = "添加捷径";
|
||||
"skills.install.confirmAdded" = "我已添加";
|
||||
|
||||
/* 润色风格包 */
|
||||
"polishStyles.title" = "润色风格";
|
||||
"polishStyles.add" = "添加风格";
|
||||
|
||||
Reference in New Issue
Block a user