feat(coding-agent): 语音 Agent 设置 UI(开关 + Claude/OpenCode 选择 + 双热键 + 模型/权限/工作目录)

- 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) <noreply@anthropic.com>
This commit is contained in:
吕柏青andClaude Opus 4.8 committed 2026-06-05 08:20:56 +08:00
1 parent d5756db8a6
commit d8dc54695d
11 files changed
+352 -5

No files matched your search

+71
View File
@@ -611,6 +611,27 @@ pub struct UserPreferences {
/// 「唤起 App」全局快捷键。`None` = 停用;`Some(...)` = 注册。默认 `Some(默认键)`。
#[serde(default = "default_open_app_hotkey")]
pub open_app_hotkey: Option<ShortcutBinding>,
/// 快速 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<String>,
/// 权限模式: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<String>,
/// 热键 1:语音 Agent 面板键。默认 Cmd/Ctrl+Shift+Enter。`None` = 停用。
#[serde(default = "default_coding_agent_panel_hotkey")]
pub coding_agent_panel_hotkey: Option<ShortcutBinding>,
/// 热键 2:快取用键(选中→Claude→回插)。默认 `None`(用户自配)。
#[serde(default)]
pub coding_agent_quick_hotkey: Option<ShortcutBinding>,
/// 本地 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<ShortcutBinding>,
switch_style_hotkey: Option<ShortcutBinding>,
open_app_hotkey: Option<ShortcutBinding>,
#[serde(default)]
coding_agent_enabled: bool,
#[serde(default = "default_coding_agent_provider")]
coding_agent_provider: String,
#[serde(default)]
coding_agent_model: Option<String>,
#[serde(default = "default_coding_agent_permission_mode")]
coding_agent_permission_mode: String,
#[serde(default)]
coding_agent_workdir: Option<String>,
#[serde(default = "default_coding_agent_panel_hotkey")]
coding_agent_panel_hotkey: Option<ShortcutBinding>,
#[serde(default)]
coding_agent_quick_hotkey: Option<ShortcutBinding>,
#[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<ShortcutBinding> {
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<ShortcutBinding> {
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(),
+14
View File
@@ -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.',
+14
View File
@@ -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: '認識の問題を調査するときに使用。通常はオフのままで構いません。',
+14
View File
@@ -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: '인식 문제를 진단할 때 사용합니다. 평소에는 꺼두어도 됩니다.',
+14
View File
@@ -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: '排查识别问题时使用,平时无需开启。',
+14
View File
@@ -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: '排查辨識問題時使用,平時無需開啟。',
+9 -5
View File
@@ -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"
@@ -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,
+21
View File
@@ -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')。 */
@@ -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 (
<Card>
<div style={{ fontSize: 12, color: 'var(--ol-ink-4)' }}>{t('common.loading')}</div>
</Card>
)
}
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 (
<SettingRow label={label} desc={desc}>
{value ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, width: '100%' }}>
<ShortcutRecorder value={value} alignRecordButton onSave={b => setHotkey(key, b)} />
<button
onClick={() => void setHotkey(key, null)}
style={{
alignSelf: 'flex-start',
fontSize: 11,
padding: '3px 10px',
background: 'transparent',
color: 'var(--ol-ink-4)',
border: '0.5px solid var(--ol-line-strong)',
borderRadius: 6,
fontFamily: 'inherit',
cursor: 'default',
}}
>
{t('settings.shortcuts.disable', 'Disable')}
</button>
</div>
) : (
<Btn variant="blue" size="sm" onClick={() => void setHotkey(key, fallback)}>
{t('settings.shortcuts.enable', 'Enable')}
</Btn>
)}
</SettingRow>
)
}
return (
<Card>
<SectionTitle>{t('settings.codingAgent.title')}</SectionTitle>
<SectionDesc>{t('settings.codingAgent.desc')}</SectionDesc>
<SettingRow label={t('settings.codingAgent.enable')} desc={t('settings.codingAgent.comingSoonNote')}>
<Toggle
on={enabled}
onToggle={next => void savePrefs({ ...prefs, codingAgentEnabled: next })}
/>
</SettingRow>
{enabled && (
<>
<SettingRow label={t('settings.codingAgent.provider')}>
<select
value={prefs.codingAgentProvider}
onChange={e =>
void savePrefs({
...prefs,
codingAgentProvider: e.target.value as CodingAgentProviderId,
})
}
style={{ ...inputStyle, maxWidth: 240, cursor: 'pointer' }}
>
<option value="claude-code-cli">Claude Code</option>
<option value="opencode-cli">{t('settings.codingAgent.providerOpenCodeSoon')}</option>
</select>
</SettingRow>
{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,
)}
<SettingRow label={t('settings.codingConsole.permissionMode')}>
<select
value={prefs.codingAgentPermissionMode}
onChange={e =>
void savePrefs({
...prefs,
codingAgentPermissionMode: e.target.value as CodingAgentPermissionMode,
})
}
style={{ ...inputStyle, maxWidth: 240, cursor: 'pointer' }}
>
{PERMISSION_MODES.map(m => (
<option key={m} value={m}>
{t(`settings.codingConsole.mode.${m}`)}
</option>
))}
</select>
</SettingRow>
<SettingRow label={t('settings.codingAgent.model')}>
<input
type="text"
value={prefs.codingAgentModel ?? ''}
placeholder={t('settings.codingAgent.modelPlaceholder')}
spellCheck={false}
onChange={e => {
const v = e.target.value.trim()
void savePrefs({ ...prefs, codingAgentModel: v === '' ? null : v })
}}
style={inputStyle}
/>
</SettingRow>
<SettingRow label={t('settings.codingConsole.workdir')} desc={t('settings.codingConsole.workdirDesc')}>
<input
type="text"
value={prefs.codingAgentWorkdir ?? ''}
placeholder={t('settings.codingConsole.workdirPlaceholder')}
spellCheck={false}
onChange={e => {
const v = e.target.value.trim()
void savePrefs({ ...prefs, codingAgentWorkdir: v === '' ? null : v })
}}
style={inputStyle}
/>
</SettingRow>
</>
)}
</Card>
)
}
@@ -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() {
<>
<LocalModelSection />
<DebugToolsSection />
<CodingAgentSection />
<ClaudeConsoleSection />
<BetaChannelSection />
</>