From d8dc54695dcafd1472c796094d714306834cf291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=95=E6=9F=8F=E9=9D=92?= Date: Fri, 5 Jun 2026 08:20:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(coding-agent):=20=E8=AF=AD=E9=9F=B3=20Agen?= =?UTF-8?q?t=20=E8=AE=BE=E7=BD=AE=20UI=EF=BC=88=E5=BC=80=E5=85=B3=20+=20Cl?= =?UTF-8?q?aude/OpenCode=20=E9=80=89=E6=8B=A9=20+=20=E5=8F=8C=E7=83=AD?= =?UTF-8?q?=E9=94=AE=20+=20=E6=A8=A1=E5=9E=8B/=E6=9D=83=E9=99=90/=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E7=9B=AE=E5=BD=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserPreferences 新增 7 字段(coding_agent_*):启用开关、后端 provider、模型、 权限模式、工作目录、面板热键、快取用热键;贯穿 struct/Wire/双向转换/默认。 - 前端 types.ts/ipc.ts 同步类型与 mock;CodingAgentPermissionMode 收敛到 types.ts 单一来源。 - 新增 CodingAgentSection(设置→高级):启用开关、Claude/OpenCode 选择、两个 ShortcutRecorder(可启用/停用)、权限模式、模型、工作目录。 - i18n 全 5 语言新增 settings.codingAgent.*。 热键实时注册与触发执行链路(coordinator 接线)随后续提交。 Co-Authored-By: Claude Opus 4.8 (1M context) --- openless-all/app/src-tauri/src/types.rs | 71 ++++++++ openless-all/app/src/i18n/en.ts | 14 ++ openless-all/app/src/i18n/ja.ts | 14 ++ openless-all/app/src/i18n/ko.ts | 14 ++ openless-all/app/src/i18n/zh-CN.ts | 14 ++ openless-all/app/src/i18n/zh-TW.ts | 14 ++ openless-all/app/src/lib/ipc.ts | 14 +- openless-all/app/src/lib/stylePrefs.test.ts | 7 + openless-all/app/src/lib/types.ts | 21 +++ .../src/pages/settings/CodingAgentSection.tsx | 172 ++++++++++++++++++ openless-all/app/src/pages/settings/tabs.tsx | 2 + 11 files changed, 352 insertions(+), 5 deletions(-) create mode 100644 openless-all/app/src/pages/settings/CodingAgentSection.tsx diff --git a/openless-all/app/src-tauri/src/types.rs b/openless-all/app/src-tauri/src/types.rs index 5f8c16a3..891ddc74 100644 --- a/openless-all/app/src-tauri/src/types.rs +++ b/openless-all/app/src-tauri/src/types.rs @@ -611,6 +611,27 @@ pub struct UserPreferences { /// 「唤起 App」全局快捷键。`None` = 停用;`Some(...)` = 注册。默认 `Some(默认键)`。 #[serde(default = "default_open_app_hotkey")] pub open_app_hotkey: Option, + /// 快速 Agent(Voice Coding Agent):是否启用。默认关闭,需用户在高级设置开启。 + #[serde(default)] + pub coding_agent_enabled: bool, + /// Agent 后端:`claude-code-cli`(默认)或 `opencode-cli`。 + #[serde(default = "default_coding_agent_provider")] + pub coding_agent_provider: String, + /// Agent 模型(`None` = 运行时取便宜默认 sonnet)。 + #[serde(default)] + pub coding_agent_model: Option, + /// 权限模式:plan/default/acceptEdits/bypassPermissions。默认 acceptEdits(放行+护栏)。 + #[serde(default = "default_coding_agent_permission_mode")] + pub coding_agent_permission_mode: String, + /// Agent 工作目录(`None` = 临时目录)。 + #[serde(default)] + pub coding_agent_workdir: Option, + /// 热键 1:语音 Agent 面板键。默认 Cmd/Ctrl+Shift+Enter。`None` = 停用。 + #[serde(default = "default_coding_agent_panel_hotkey")] + pub coding_agent_panel_hotkey: Option, + /// 热键 2:快取用键(选中→Claude→回插)。默认 `None`(用户自配)。 + #[serde(default)] + pub coding_agent_quick_hotkey: Option, /// 本地 Qwen3-ASR 当前激活的模型 id("qwen3-asr-0.6b" / "qwen3-asr-1.7b")。 /// 仅在 active_asr_provider == "local-qwen3" 时有意义。 #[serde(default = "default_local_asr_model")] @@ -809,6 +830,20 @@ struct UserPreferencesWire { translation_hotkey: Option, switch_style_hotkey: Option, open_app_hotkey: Option, + #[serde(default)] + coding_agent_enabled: bool, + #[serde(default = "default_coding_agent_provider")] + coding_agent_provider: String, + #[serde(default)] + coding_agent_model: Option, + #[serde(default = "default_coding_agent_permission_mode")] + coding_agent_permission_mode: String, + #[serde(default)] + coding_agent_workdir: Option, + #[serde(default = "default_coding_agent_panel_hotkey")] + coding_agent_panel_hotkey: Option, + #[serde(default)] + coding_agent_quick_hotkey: Option, #[serde(default = "default_local_asr_model")] local_asr_active_model: String, #[serde(default = "default_local_asr_mirror")] @@ -892,6 +927,13 @@ impl Default for UserPreferencesWire { // 默认携带默认键(Some),保证缺字段时仍是启用状态;None 专表「用户主动停用」。 switch_style_hotkey: prefs.switch_style_hotkey, open_app_hotkey: prefs.open_app_hotkey, + coding_agent_enabled: prefs.coding_agent_enabled, + coding_agent_provider: prefs.coding_agent_provider, + coding_agent_model: prefs.coding_agent_model, + coding_agent_permission_mode: prefs.coding_agent_permission_mode, + coding_agent_workdir: prefs.coding_agent_workdir, + coding_agent_panel_hotkey: prefs.coding_agent_panel_hotkey, + coding_agent_quick_hotkey: prefs.coding_agent_quick_hotkey, local_asr_active_model: prefs.local_asr_active_model, local_asr_mirror: prefs.local_asr_mirror, local_asr_keep_loaded_secs: prefs.local_asr_keep_loaded_secs, @@ -968,6 +1010,13 @@ impl<'de> Deserialize<'de> for UserPreferences { output_language_preference: wire.output_language_preference, qa_hotkey: wire.qa_hotkey, qa_save_history: wire.qa_save_history, + coding_agent_enabled: wire.coding_agent_enabled, + coding_agent_provider: wire.coding_agent_provider, + coding_agent_model: wire.coding_agent_model, + coding_agent_permission_mode: wire.coding_agent_permission_mode, + coding_agent_workdir: wire.coding_agent_workdir, + coding_agent_panel_hotkey: wire.coding_agent_panel_hotkey, + coding_agent_quick_hotkey: wire.coding_agent_quick_hotkey, custom_combo_hotkey: wire.custom_combo_hotkey, translation_hotkey: wire .translation_hotkey @@ -1012,6 +1061,21 @@ fn default_qa_hotkey() -> Option { Some(ShortcutBinding::default_qa()) } +fn default_coding_agent_provider() -> String { + "claude-code-cli".to_string() +} + +fn default_coding_agent_permission_mode() -> String { + "acceptEdits".to_string() +} + +fn default_coding_agent_panel_hotkey() -> Option { + Some(ShortcutBinding { + primary: "Enter".into(), + modifiers: vec!["cmd".into(), "shift".into()], + }) +} + fn default_translation_hotkey() -> ShortcutBinding { ShortcutBinding { primary: "Shift".into(), @@ -1670,6 +1734,13 @@ impl Default for UserPreferences { translation_hotkey: default_translation_hotkey(), switch_style_hotkey: default_switch_style_hotkey(), open_app_hotkey: default_open_app_hotkey(), + coding_agent_enabled: false, + coding_agent_provider: default_coding_agent_provider(), + coding_agent_model: None, + coding_agent_permission_mode: default_coding_agent_permission_mode(), + coding_agent_workdir: None, + coding_agent_panel_hotkey: default_coding_agent_panel_hotkey(), + coding_agent_quick_hotkey: None, local_asr_active_model: default_local_asr_model(), local_asr_mirror: default_local_asr_mirror(), local_asr_keep_loaded_secs: default_local_asr_keep_loaded_secs(), diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index b1512025..23038848 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -535,6 +535,20 @@ export const en: typeof zhCN = { cancelled: 'Cancelled', outputPlaceholder: 'Output streams here…', }, + codingAgent: { + title: 'Voice Coding Agent', + desc: 'Trigger Claude Code (or OpenCode) by a global hotkey to run quick tasks. Off by default; configure it here.', + enable: 'Enable Voice Coding Agent', + comingSoonNote: 'Config is saved now; hotkey triggering and the execution flow land in a later version.', + provider: 'Agent backend', + providerOpenCodeSoon: 'OpenCode (coming soon)', + panelHotkey: 'Panel hotkey (voice agent)', + panelHotkeyDesc: 'Record voice → ASR → Claude → streamed into a panel. Default Cmd/Ctrl+Shift+Enter.', + quickHotkey: 'Quick-take hotkey', + quickHotkeyDesc: 'Take selected text → Claude → result back at the cursor. No panel, faster.', + model: 'Model', + modelPlaceholder: 'Default: sonnet', + }, debug: { title: 'Debug tools', desc: 'For troubleshooting recognition issues; off by default.', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 42d1b735..83ee205a 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -537,6 +537,20 @@ export const ja: typeof zhCN = { cancelled: 'キャンセル済み', outputPlaceholder: '出力はここにストリーミング表示されます…', }, + codingAgent: { + title: '音声 Agent(クイック Agent)', + desc: 'グローバルホットキーで Claude Code(または OpenCode)を直接起動してクイックタスクを実行。デフォルト無効、ここで設定。', + enable: '音声 Agent を有効化', + comingSoonNote: '設定はすぐ保存されます。ホットキー起動と実行フローは今後のバージョンで対応。', + provider: 'Agent バックエンド', + providerOpenCodeSoon: 'OpenCode(近日対応)', + panelHotkey: 'パネルキー(音声 Agent)', + panelHotkeyDesc: '録音 → ASR → Claude → パネルにストリーミング表示。デフォルト Cmd/Ctrl+Shift+Enter。', + quickHotkey: 'クイック取得キー', + quickHotkeyDesc: '選択テキストを取得 → Claude → 結果をカーソル位置へ。パネルなし、より高速。', + model: 'モデル', + modelPlaceholder: 'デフォルト: sonnet', + }, debug: { title: 'デバッグツール', desc: '認識の問題を調査するときに使用。通常はオフのままで構いません。', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 7159174c..5a1f56eb 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -537,6 +537,20 @@ export const ko: typeof zhCN = { cancelled: '취소됨', outputPlaceholder: '출력이 여기에 스트리밍됩니다…', }, + codingAgent: { + title: '음성 Agent(빠른 Agent)', + desc: '전역 단축키로 Claude Code(또는 OpenCode)를 바로 실행해 빠른 작업 수행. 기본 비활성, 여기서 설정.', + enable: '음성 Agent 활성화', + comingSoonNote: '설정은 즉시 저장됩니다. 단축키 트리거와 실행 흐름은 이후 버전에서 제공됩니다.', + provider: 'Agent 백엔드', + providerOpenCodeSoon: 'OpenCode(곧 지원)', + panelHotkey: '패널 키(음성 Agent)', + panelHotkeyDesc: '녹음 → ASR → Claude → 패널에 스트리밍. 기본 Cmd/Ctrl+Shift+Enter.', + quickHotkey: '빠른 가져오기 키', + quickHotkeyDesc: '선택한 텍스트 → Claude → 결과를 커서 위치로. 패널 없이 더 빠름.', + model: '모델', + modelPlaceholder: '기본: sonnet', + }, debug: { title: '디버그 도구', desc: '인식 문제를 진단할 때 사용합니다. 평소에는 꺼두어도 됩니다.', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index 31e29f25..7d61e926 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -533,6 +533,20 @@ export const zhCN = { cancelled: '已取消', outputPlaceholder: '输出会流式显示在这里…', }, + codingAgent: { + title: '语音 Agent(快速 Agent)', + desc: '按全局热键直接触发 Claude Code(或 OpenCode)执行快速任务。默认关闭,在此配置。', + enable: '启用语音 Agent', + comingSoonNote: '配置即时保存;热键触发与执行链路随后续版本生效。', + provider: 'Agent 后端', + providerOpenCodeSoon: 'OpenCode(即将支持)', + panelHotkey: '面板键(语音 Agent)', + panelHotkeyDesc: '录音 → ASR → Claude → 结果流式进面板。默认 Cmd/Ctrl+Shift+Enter。', + quickHotkey: '快取用键', + quickHotkeyDesc: '拿当前选中文本 → Claude → 结果回插光标处。不开面板、更快。', + model: '模型', + modelPlaceholder: '默认 sonnet', + }, debug: { title: '调试工具', desc: '排查识别问题时使用,平时无需开启。', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index 42dde898..fc0636f9 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -535,6 +535,20 @@ export const zhTW: typeof zhCN = { cancelled: '已取消', outputPlaceholder: '輸出會串流顯示在這裡…', }, + codingAgent: { + title: '語音 Agent(快速 Agent)', + desc: '按全域熱鍵直接觸發 Claude Code(或 OpenCode)執行快速任務。預設關閉,在此設定。', + enable: '啟用語音 Agent', + comingSoonNote: '設定即時儲存;熱鍵觸發與執行鏈路隨後續版本生效。', + provider: 'Agent 後端', + providerOpenCodeSoon: 'OpenCode(即將支援)', + panelHotkey: '面板鍵(語音 Agent)', + panelHotkeyDesc: '錄音 → ASR → Claude → 結果串流進面板。預設 Cmd/Ctrl+Shift+Enter。', + quickHotkey: '快取用鍵', + quickHotkeyDesc: '取目前選取文字 → Claude → 結果回插游標處。不開面板、更快。', + model: '模型', + modelPlaceholder: '預設 sonnet', + }, debug: { title: '除錯工具', desc: '排查辨識問題時使用,平時無需開啟。', diff --git a/openless-all/app/src/lib/ipc.ts b/openless-all/app/src/lib/ipc.ts index 7c5d5c37..af083cc5 100644 --- a/openless-all/app/src/lib/ipc.ts +++ b/openless-all/app/src/lib/ipc.ts @@ -15,6 +15,7 @@ import type { HotkeyStatus, MicrophoneDevice, PermissionStatus, + CodingAgentPermissionMode, PolishMode, QaHotkeyBinding, ShortcutBinding, @@ -100,6 +101,13 @@ let mockSettings: UserPreferences = { modifiers: defaultAppShortcutModifiers(), }, openAppHotkey: { primary: "O", modifiers: defaultAppShortcutModifiers() }, + codingAgentEnabled: false, + codingAgentProvider: "claude-code-cli", + codingAgentModel: null, + codingAgentPermissionMode: "acceptEdits", + codingAgentWorkdir: null, + codingAgentPanelHotkey: { primary: "Enter", modifiers: ["cmd", "shift"] }, + codingAgentQuickHotkey: null, localAsrActiveModel: "qwen3-asr-0.6b", localAsrMirror: "huggingface", localAsrKeepLoadedSecs: 300, @@ -1153,11 +1161,7 @@ export async function exportErrorLog( export { isTauri } // ── Coding Agent / Claude 控制台 ─────────────────────────────────────── -export type CodingAgentPermissionMode = - | "plan" - | "default" - | "acceptEdits" - | "bypassPermissions" +export type { CodingAgentPermissionMode } export type McpHealth = "connected" | "failed" | "needs_auth" | "unknown" diff --git a/openless-all/app/src/lib/stylePrefs.test.ts b/openless-all/app/src/lib/stylePrefs.test.ts index 66d08b68..e5c93593 100644 --- a/openless-all/app/src/lib/stylePrefs.test.ts +++ b/openless-all/app/src/lib/stylePrefs.test.ts @@ -49,6 +49,13 @@ const previousPrefs: UserPreferences = { translationHotkey: { primary: 'Shift', modifiers: [] }, switchStyleHotkey: { primary: 'S', modifiers: ['alt'] }, openAppHotkey: { primary: 'O', modifiers: ['alt'] }, + codingAgentEnabled: false, + codingAgentProvider: 'claude-code-cli', + codingAgentModel: null, + codingAgentPermissionMode: 'acceptEdits', + codingAgentWorkdir: null, + codingAgentPanelHotkey: { primary: 'Enter', modifiers: ['cmd', 'shift'] }, + codingAgentQuickHotkey: null, localAsrActiveModel: '', localAsrMirror: 'huggingface', localAsrKeepLoadedSecs: 300, diff --git a/openless-all/app/src/lib/types.ts b/openless-all/app/src/lib/types.ts index 9e1e660c..f9c212a7 100644 --- a/openless-all/app/src/lib/types.ts +++ b/openless-all/app/src/lib/types.ts @@ -117,6 +117,13 @@ export type QaHotkeyBinding = ShortcutBinding; /** 自定义录音组合键绑定。当 hotkey.trigger == 'custom' 时使用。 */ export type ComboBinding = ShortcutBinding; +export type CodingAgentProviderId = "claude-code-cli" | "opencode-cli"; +export type CodingAgentPermissionMode = + | "plan" + | "default" + | "acceptEdits" + | "bypassPermissions"; + /** 模拟粘贴时按下的快捷键。仅 Windows/Linux 生效;macOS 走 AX 直写。 * - ctrlV : 标准粘贴(默认;大多数编辑器、浏览器、IDE) * - ctrlShiftV : kitty / alacritty / wezterm / gnome-terminal / foot 等终端 @@ -259,6 +266,20 @@ export interface UserPreferences { switchStyleHotkey: ShortcutBinding | null; /** 打开 OpenLess 主窗口的全局快捷键。null = 用户已停用(issue #576)。 */ openAppHotkey: ShortcutBinding | null; + /** 快速 Agent(Voice Coding Agent):是否启用。默认关闭。 */ + codingAgentEnabled: boolean; + /** Agent 后端:claude-code-cli(默认)/ opencode-cli。 */ + codingAgentProvider: CodingAgentProviderId; + /** Agent 模型,null = 运行时取便宜默认(sonnet)。 */ + codingAgentModel: string | null; + /** 权限模式:plan/default/acceptEdits/bypassPermissions。 */ + codingAgentPermissionMode: CodingAgentPermissionMode; + /** Agent 工作目录,null = 临时目录。 */ + codingAgentWorkdir: string | null; + /** 热键 1:语音 Agent 面板键。null = 停用。 */ + codingAgentPanelHotkey: ShortcutBinding | null; + /** 热键 2:快取用键(选中→Claude→回插)。null = 未配置。 */ + codingAgentQuickHotkey: ShortcutBinding | null; /** 本地 Qwen3-ASR 当前激活的模型 id。仅在 activeAsrProvider === 'local-qwen3' 时有意义。 */ localAsrActiveModel: string; /** 本地模型下载源镜像('huggingface' / 'hf-mirror')。 */ diff --git a/openless-all/app/src/pages/settings/CodingAgentSection.tsx b/openless-all/app/src/pages/settings/CodingAgentSection.tsx new file mode 100644 index 00000000..4bf4905f --- /dev/null +++ b/openless-all/app/src/pages/settings/CodingAgentSection.tsx @@ -0,0 +1,172 @@ +// 高级 → 语音 Agent(快速 Agent)配置:启用开关、后端(Claude / OpenCode)、 +// 两个全局热键、模型 / 权限模式 / 工作目录。配置经 UserPreferences 持久化; +// 热键的实时注册与触发链路由 coordinator 接线(随后续版本生效)。 + +import { useTranslation } from 'react-i18next' +import type { CodingAgentPermissionMode, CodingAgentProviderId, ShortcutBinding } from '../../lib/types' +import { useHotkeySettings } from '../../state/HotkeySettingsContext' +import { ShortcutRecorder } from '../../components/ShortcutRecorder' +import { Btn, Card } from '../_atoms' +import { SectionDesc, SectionTitle, SettingRow, Toggle, inputStyle } from './shared' + +const PERMISSION_MODES: CodingAgentPermissionMode[] = [ + 'acceptEdits', + 'plan', + 'default', + 'bypassPermissions', +] + +const DEFAULT_PANEL_HOTKEY: ShortcutBinding = { primary: 'Enter', modifiers: ['cmd', 'shift'] } +const DEFAULT_QUICK_HOTKEY: ShortcutBinding = { primary: 'J', modifiers: ['cmd', 'shift'] } + +export function CodingAgentSection() { + const { t } = useTranslation() + const { prefs, updatePrefs: savePrefs } = useHotkeySettings() + + if (!prefs) { + return ( + +
{t('common.loading')}
+
+ ) + } + + const enabled = prefs.codingAgentEnabled + + const setHotkey = ( + key: 'codingAgentPanelHotkey' | 'codingAgentQuickHotkey', + binding: ShortcutBinding | null, + ) => savePrefs({ ...prefs, [key]: binding }) + + const hotkeyRow = ( + key: 'codingAgentPanelHotkey' | 'codingAgentQuickHotkey', + label: string, + desc: string, + fallback: ShortcutBinding, + ) => { + const value = prefs[key] + return ( + + {value ? ( +
+ setHotkey(key, b)} /> + +
+ ) : ( + void setHotkey(key, fallback)}> + {t('settings.shortcuts.enable', 'Enable')} + + )} +
+ ) + } + + return ( + + {t('settings.codingAgent.title')} + {t('settings.codingAgent.desc')} + + + void savePrefs({ ...prefs, codingAgentEnabled: next })} + /> + + + {enabled && ( + <> + + + + + {hotkeyRow( + 'codingAgentPanelHotkey', + t('settings.codingAgent.panelHotkey'), + t('settings.codingAgent.panelHotkeyDesc'), + DEFAULT_PANEL_HOTKEY, + )} + {hotkeyRow( + 'codingAgentQuickHotkey', + t('settings.codingAgent.quickHotkey'), + t('settings.codingAgent.quickHotkeyDesc'), + DEFAULT_QUICK_HOTKEY, + )} + + + + + + + { + const v = e.target.value.trim() + void savePrefs({ ...prefs, codingAgentModel: v === '' ? null : v }) + }} + style={inputStyle} + /> + + + + { + const v = e.target.value.trim() + void savePrefs({ ...prefs, codingAgentWorkdir: v === '' ? null : v }) + }} + style={inputStyle} + /> + + + )} + + ) +} diff --git a/openless-all/app/src/pages/settings/tabs.tsx b/openless-all/app/src/pages/settings/tabs.tsx index 286039b2..2be23140 100644 --- a/openless-all/app/src/pages/settings/tabs.tsx +++ b/openless-all/app/src/pages/settings/tabs.tsx @@ -11,6 +11,7 @@ import { PermissionsSection } from './PermissionsSection'; import { DataStorageSection } from './DataStorageSection'; import { LocalModelSection } from './LocalModelSection'; import { DebugToolsSection } from './DebugToolsSection'; +import { CodingAgentSection } from './CodingAgentSection'; import { ClaudeConsoleSection } from './ClaudeConsoleSection'; import { BetaChannelSection } from './BetaChannelSection'; @@ -73,6 +74,7 @@ export function AdvancedTab() { <> +