feat(coding-agent): 拆分两功能 — 选中润色(快取用键)+ Cloud Agent(语音键,占位);热键移到通用设置

- 快取用键 = 选中文本润色:选中→Claude 润色→回插替换;全程胶囊反馈
  (没选中提示"请先选中文本";润色中/完成/出错都显示),纯文本 plan 只读、无 guard、便宜可靠。
- 面板键 = Cloud Agent(语音→ASR→Claude→结果弹窗),完整流程开发中,先给可见提示。
- 两个热键从「高级」移到「通用→快捷键」,停用按钮置于录制器左边、常驻行。
- CodingAgentSection 精简为开关/后端/模型/权限/工作目录 + 热键位置提示;5 语言 i18n。

cargo check + tsc + vite 均绿。

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 09:21:35 +08:00
1 parent 0463c30282
commit 8211dea3d7
8 files changed
+173 -129

No files matched your search

+80 -63
View File
@@ -1404,77 +1404,90 @@ fn coding_agent_hotkey_bridge_loop(inner: Arc<Inner>, rx: mpsc::Receiver<CodingA
}
let inner_cloned = Arc::clone(&inner);
async_runtime::spawn(async move {
// 本版面板键与快取用键都走「选中→Claude→回插」;语音 + 面板随后续版本。
let _ = evt;
handle_coding_agent_quick(&inner_cloned).await;
match evt {
// 快取用键 = 选中文本润色(选中→Claude 润色→回插替换)。
CodingAgentHotkeyEvent::QuickPressed => {
handle_coding_agent_quick(&inner_cloned).await;
}
// 面板键 = Cloud Agent(语音→ASR→Claude→结果弹窗),完整流程开发中。
CodingAgentHotkeyEvent::PanelPressed => {
handle_coding_agent_panel(&inner_cloned).await;
}
}
});
}
}
/// 快取用:抓当前选中文本作为指令 → 护栏化无头 Claude → 结果回插光标处。
/// 面板键 = Cloud Agent:录音 → ASR → Claude 无头跑小任务 → 结果弹窗。
/// 完整语音流程开发中,先给用户一个明确的可见提示,避免静默。
async fn handle_coding_agent_panel(inner: &Arc<Inner>) {
log::info!("[coding-agent] 面板键:Cloud Agent 语音功能开发中");
emit_capsule(
inner,
CapsuleState::Error,
0.0,
0,
Some("Cloud Agent 语音功能开发中,敬请期待".to_string()),
None,
);
}
/// 快取用:抓当前选中文本 → Claude 润色 → 回插(替换选区)。全程胶囊反馈。
async fn handle_coding_agent_quick(inner: &Arc<Inner>) {
let prefs = inner.prefs.get();
if !prefs.coding_agent_enabled {
return;
}
let selection =
tauri::async_runtime::spawn_blocking(crate::selection::capture_selection)
.await
.ok()
.flatten();
let prompt = match selection {
let selection = tauri::async_runtime::spawn_blocking(crate::selection::capture_selection)
.await
.ok()
.flatten();
let source_text = match selection {
Some(ctx) => ctx.text,
None => {
log::info!("[coding-agent] 快取用:没有选中文本,忽略");
log::info!("[coding-agent] 快取用:没有选中文本");
emit_capsule(
inner,
CapsuleState::Error,
0.0,
0,
Some("请先选中文本,再按快捷键".to_string()),
None,
);
return;
}
};
let mode = prefs.coding_agent_permission_mode.clone();
let settings_json = crate::coding_agent::guard::build_guard_settings_json(&mode, &[]);
let settings_path =
std::env::temp_dir().join(format!("openless-agent-guard-{}.json", uuid::Uuid::new_v4()));
if std::fs::write(
&settings_path,
serde_json::to_vec(&settings_json).unwrap_or_default(),
)
.is_err()
{
log::warn!("[coding-agent] 写护栏配置失败");
return;
}
log::info!(
"[coding-agent] 快取用:润色 {} 字",
source_text.chars().count()
);
emit_capsule(
inner,
CapsuleState::Polishing,
0.0,
0,
Some("Claude 润色中…".to_string()),
None,
);
let cwd = prefs
.coding_agent_workdir
.clone()
.filter(|w| !w.trim().is_empty())
.map(std::path::PathBuf::from);
if let Some(dir) = &cwd {
if let Some(sha) = crate::coding_agent::create_git_snapshot(dir) {
log::info!("[coding-agent] 运行前 git 快照 {sha}");
}
}
let prompt = format!(
"请润色下面这段文字,使其更通顺自然、表达更清晰,保持原意、语言和事实不变。\
直接输出润色后的文本,不要加任何解释、前缀或引号:\n\n{source_text}"
);
let mut req = crate::coding_agent::CodingAgentRequest::new("quick", prompt);
req.cwd = cwd;
// 纯文本润色:不需要任何工具 → plan 只读、无 guard、便宜快、最可靠。
let mut req = crate::coding_agent::CodingAgentRequest::new("quick-polish", prompt);
req.model = prefs
.coding_agent_model
.clone()
.filter(|m| !m.trim().is_empty())
.or_else(|| Some("sonnet".to_string()));
req.permission_mode = parse_coding_agent_permission_mode(&mode);
req.settings_json_path = Some(settings_path.clone());
req.max_budget_usd = Some(0.5);
req.timeout_secs = 120;
req.permission_mode = crate::coding_agent::CodingAgentPermissionMode::Plan;
req.allowed_tools = Vec::new();
req.max_budget_usd = Some(0.2);
req.timeout_secs = 60;
req.session_persistence = false;
req.allowed_tools = vec![
"Bash".into(),
"Read".into(),
"Edit".into(),
"Write".into(),
"Glob".into(),
"Grep".into(),
];
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let cancel = Arc::new(std::sync::atomic::AtomicBool::new(false));
@@ -1483,20 +1496,32 @@ async fn handle_coding_agent_quick(inner: &Arc<Inner>) {
});
let mut final_text = String::new();
let mut error_msg: Option<String> = None;
while let Some(ev) = rx.recv().await {
if let crate::coding_agent::CodingAgentEvent::Completed { text, .. } = ev {
final_text = text;
match ev {
crate::coding_agent::CodingAgentEvent::Completed { text, .. } => final_text = text,
crate::coding_agent::CodingAgentEvent::Error { message, .. } => {
error_msg = Some(message)
}
_ => {}
}
}
let _ = run.await;
let _ = std::fs::remove_file(&settings_path);
let run_result = run.await;
let final_text = final_text.trim().to_string();
if final_text.is_empty() {
log::info!("[coding-agent] 快取用:Claude 无文本结果");
let msg = error_msg
.or_else(|| match run_result {
Ok(Err(e)) => Some(e.to_string()),
_ => None,
})
.unwrap_or_else(|| "Claude 无结果(确认已登录 claude 且额度充足)".to_string());
log::warn!("[coding-agent] 快取用失败: {msg}");
emit_capsule(inner, CapsuleState::Error, 0.0, 0, Some(msg), None);
return;
}
let inserted = final_text.chars().count() as u32;
let inner2 = Arc::clone(inner);
let restore = prefs.restore_clipboard_after_paste;
let paste_shortcut = prefs.paste_shortcut;
@@ -1504,16 +1529,8 @@ async fn handle_coding_agent_quick(inner: &Arc<Inner>) {
inner2.inserter.insert(&final_text, restore, paste_shortcut)
})
.await;
}
fn parse_coding_agent_permission_mode(s: &str) -> crate::coding_agent::CodingAgentPermissionMode {
use crate::coding_agent::CodingAgentPermissionMode as M;
match s {
"plan" => M::Plan,
"default" => M::Default,
"bypassPermissions" => M::BypassPermissions,
_ => M::AcceptEdits,
}
log::info!("[coding-agent] 快取用:已回插 {inserted} 字");
emit_capsule(inner, CapsuleState::Done, 0.0, 0, None, Some(inserted));
}
fn combo_hotkey_supervisor_loop(inner: Arc<Inner>) {
+5
View File
@@ -540,6 +540,7 @@ export const en: typeof zhCN = {
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.',
hotkeyHint: 'Set the two hotkeys under General → Shortcuts.',
provider: 'Agent backend',
providerOpenCodeSoon: 'OpenCode (coming soon)',
panelHotkey: 'Panel hotkey (voice agent)',
@@ -735,6 +736,10 @@ export const en: typeof zhCN = {
confirm: 'Confirm capsule insertion',
switchStyle: 'Switch to previous style',
openApp: 'Open OpenLess',
agentPolish: 'Polish selection (Claude)',
agentPolishDesc: 'Select text → press → Claude polishes it → replaces the selection.',
agentVoice: 'Cloud Agent (voice)',
agentVoiceDesc: 'Press → speak → Claude runs the task → result shown in a capsule. (Coming soon)',
enable: 'Enable',
disable: 'Disable',
confirmHint: 'Click ✓ on the capsule',
+5
View File
@@ -542,6 +542,7 @@ export const ja: typeof zhCN = {
desc: 'グローバルホットキーで Claude Code(または OpenCode)を直接起動してクイックタスクを実行。デフォルト無効、ここで設定。',
enable: '音声 Agent を有効化',
comingSoonNote: '設定はすぐ保存されます。ホットキー起動と実行フローは今後のバージョンで対応。',
hotkeyHint: '2 つのホットキーは「一般 → ショートカット」で設定。',
provider: 'Agent バックエンド',
providerOpenCodeSoon: 'OpenCode(近日対応)',
panelHotkey: 'パネルキー(音声 Agent)',
@@ -737,6 +738,10 @@ export const ja: typeof zhCN = {
confirm: 'カプセル入力を確定',
switchStyle: '前のスタイルに切り替え',
openApp: 'OpenLess を開く',
agentPolish: '選択テキストを推敲(Claude)',
agentPolishDesc: 'テキスト選択 → キー → Claude が推敲 → 選択範囲を置換。',
agentVoice: 'Cloud Agent(音声)',
agentVoiceDesc: 'キー → 話す → Claude がタスク実行 → 結果をカプセル表示。(近日対応)',
enable: '有効化',
disable: '無効化',
confirmHint: '右側の ✓ をクリック',
+5
View File
@@ -542,6 +542,7 @@ export const ko: typeof zhCN = {
desc: '전역 단축키로 Claude Code(또는 OpenCode)를 바로 실행해 빠른 작업 수행. 기본 비활성, 여기서 설정.',
enable: '음성 Agent 활성화',
comingSoonNote: '설정은 즉시 저장됩니다. 단축키 트리거와 실행 흐름은 이후 버전에서 제공됩니다.',
hotkeyHint: '두 단축키는 「일반 → 단축키」에서 설정.',
provider: 'Agent 백엔드',
providerOpenCodeSoon: 'OpenCode(곧 지원)',
panelHotkey: '패널 키(음성 Agent)',
@@ -737,6 +738,10 @@ export const ko: typeof zhCN = {
confirm: '캡슐 입력 확정',
switchStyle: '이전 스타일로 전환',
openApp: 'OpenLess 열기',
agentPolish: '선택 텍스트 다듬기(Claude)',
agentPolishDesc: '텍스트 선택 → 키 → Claude 다듬기 → 선택 영역 교체.',
agentVoice: 'Cloud Agent(음성)',
agentVoiceDesc: '키 → 말하기 → Claude 작업 실행 → 결과 캡슐 표시. (곧 지원)',
enable: '활성화',
disable: '비활성화',
confirmHint: '오른쪽 ✓ 클릭',
+5
View File
@@ -538,6 +538,7 @@ export const zhCN = {
desc: '按全局热键直接触发 Claude Code(或 OpenCode)执行快速任务。默认关闭,在此配置。',
enable: '启用语音 Agent',
comingSoonNote: '配置即时保存;热键触发与执行链路随后续版本生效。',
hotkeyHint: '两个热键在「通用 → 快捷键」里设置。',
provider: 'Agent 后端',
providerOpenCodeSoon: 'OpenCode(即将支持)',
panelHotkey: '面板键(语音 Agent)',
@@ -733,6 +734,10 @@ export const zhCN = {
confirm: '胶囊确认插入',
switchStyle: '切换上一次风格',
openApp: '打开 OpenLess',
agentPolish: '选中文本润色(Claude)',
agentPolishDesc: '选中文本 → 按键 → Claude 润色 → 替换选区。',
agentVoice: 'Cloud Agent(语音)',
agentVoiceDesc: '按键 → 说话 → Claude 执行任务 → 结果弹胶囊显示。(开发中)',
enable: '启用',
disable: '停用',
confirmHint: '点击右侧 ✓',
+5
View File
@@ -540,6 +540,7 @@ export const zhTW: typeof zhCN = {
desc: '按全域熱鍵直接觸發 Claude Code(或 OpenCode)執行快速任務。預設關閉,在此設定。',
enable: '啟用語音 Agent',
comingSoonNote: '設定即時儲存;熱鍵觸發與執行鏈路隨後續版本生效。',
hotkeyHint: '兩個熱鍵在「通用 → 快捷鍵」裡設定。',
provider: 'Agent 後端',
providerOpenCodeSoon: 'OpenCode(即將支援)',
panelHotkey: '面板鍵(語音 Agent)',
@@ -735,6 +736,10 @@ export const zhTW: typeof zhCN = {
confirm: '膠囊確認插入',
switchStyle: '切換上一次風格',
openApp: '打開 OpenLess',
agentPolish: '選取文字潤色(Claude)',
agentPolishDesc: '選取文字 → 按鍵 → Claude 潤色 → 取代選取。',
agentVoice: 'Cloud Agent(語音)',
agentVoiceDesc: '按鍵 → 說話 → Claude 執行任務 → 結果彈膠囊顯示。(開發中)',
enable: '啟用',
disable: '停用',
confirmHint: '點擊右側 ✓',
@@ -1,12 +1,11 @@
// 高级 → 语音 Agent(快速 Agent)配置:启用开关、后端(Claude / OpenCode)、
// 两个全局热键、模型 / 权限模式 / 工作目录。配置经 UserPreferences 持久化;
// 热键的实时注册与触发链路由 coordinator 接线(随后续版本生效)。
// 高级 → 快速 Agent 配置:启用开关、后端(Claude / OpenCode)、模型 / 权限模式 / 工作目录。
// 两个热键(选中润色键、Cloud Agent 语音键)在「通用 → 快捷键」里配置。
// 配置经 UserPreferences 持久化;启用后 coordinator 才注册热键。
import { useTranslation } from 'react-i18next'
import type { CodingAgentPermissionMode, CodingAgentProviderId, ShortcutBinding } from '../../lib/types'
import type { CodingAgentPermissionMode, CodingAgentProviderId } from '../../lib/types'
import { useHotkeySettings } from '../../state/HotkeySettingsContext'
import { ShortcutRecorder } from '../../components/ShortcutRecorder'
import { Btn, Card } from '../_atoms'
import { Card } from '../_atoms'
import { SectionDesc, SectionTitle, SettingRow, Toggle, inputStyle } from './shared'
const PERMISSION_MODES: CodingAgentPermissionMode[] = [
@@ -16,9 +15,6 @@ const PERMISSION_MODES: CodingAgentPermissionMode[] = [
'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()
@@ -33,55 +29,12 @@ export function CodingAgentSection() {
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')}>
<SettingRow label={t('settings.codingAgent.enable')} desc={t('settings.codingAgent.hotkeyHint')}>
<Toggle
on={enabled}
onToggle={next => void savePrefs({ ...prefs, codingAgentEnabled: next })}
@@ -106,19 +59,6 @@ export function CodingAgentSection() {
</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}
@@ -177,6 +177,68 @@ export function ShortcutsSection() {
</button>
)}
</SettingRow>
<SettingRow label={t('settings.shortcuts.agentPolish')} desc={t('settings.shortcuts.agentPolishDesc')}>
{prefs.codingAgentQuickHotkey ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}>
<button
onClick={() => void savePrefs({ ...prefs, codingAgentQuickHotkey: null })}
style={disableBtnStyle}
>
{t('settings.shortcuts.disable', 'Disable')}
</button>
<ShortcutRecorder
value={prefs.codingAgentQuickHotkey}
alignRecordButton
onSave={async b => {
await savePrefs({ ...prefs, codingAgentQuickHotkey: b });
}}
/>
</div>
) : (
<button
onClick={() =>
void savePrefs({
...prefs,
codingAgentQuickHotkey: { primary: 'J', modifiers: ['cmd', 'shift'] },
})
}
style={enableBtnStyle}
>
{t('settings.shortcuts.enable', 'Enable')}
</button>
)}
</SettingRow>
<SettingRow label={t('settings.shortcuts.agentVoice')} desc={t('settings.shortcuts.agentVoiceDesc')}>
{prefs.codingAgentPanelHotkey ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}>
<button
onClick={() => void savePrefs({ ...prefs, codingAgentPanelHotkey: null })}
style={disableBtnStyle}
>
{t('settings.shortcuts.disable', 'Disable')}
</button>
<ShortcutRecorder
value={prefs.codingAgentPanelHotkey}
alignRecordButton
onSave={async b => {
await savePrefs({ ...prefs, codingAgentPanelHotkey: b });
}}
/>
</div>
) : (
<button
onClick={() =>
void savePrefs({
...prefs,
codingAgentPanelHotkey: { primary: 'Enter', modifiers: ['cmd', 'shift'] },
})
}
style={enableBtnStyle}
>
{t('settings.shortcuts.enable', 'Enable')}
</button>
)}
</SettingRow>
{readonlyRows.map(([k, v]) => (
<SettingRow key={k} label={k}>
<kbd style={{