From 8211dea3d7b785fdf9b202fdc41e1506067bc7e4 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 09:21:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(coding-agent):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E4=B8=A4=E5=8A=9F=E8=83=BD=20=E2=80=94=20=E9=80=89=E4=B8=AD?= =?UTF-8?q?=E6=B6=A6=E8=89=B2=EF=BC=88=E5=BF=AB=E5=8F=96=E7=94=A8=E9=94=AE?= =?UTF-8?q?=EF=BC=89+=20Cloud=20Agent=EF=BC=88=E8=AF=AD=E9=9F=B3=E9=94=AE?= =?UTF-8?q?=EF=BC=8C=E5=8D=A0=E4=BD=8D=EF=BC=89=EF=BC=9B=E7=83=AD=E9=94=AE?= =?UTF-8?q?=E7=A7=BB=E5=88=B0=E9=80=9A=E7=94=A8=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 快取用键 = 选中文本润色:选中→Claude 润色→回插替换;全程胶囊反馈 (没选中提示"请先选中文本";润色中/完成/出错都显示),纯文本 plan 只读、无 guard、便宜可靠。 - 面板键 = Cloud Agent(语音→ASR→Claude→结果弹窗),完整流程开发中,先给可见提示。 - 两个热键从「高级」移到「通用→快捷键」,停用按钮置于录制器左边、常驻行。 - CodingAgentSection 精简为开关/后端/模型/权限/工作目录 + 热键位置提示;5 语言 i18n。 cargo check + tsc + vite 均绿。 Co-Authored-By: Claude Opus 4.8 (1M context) --- openless-all/app/src-tauri/src/coordinator.rs | 143 ++++++++++-------- openless-all/app/src/i18n/en.ts | 5 + openless-all/app/src/i18n/ja.ts | 5 + openless-all/app/src/i18n/ko.ts | 5 + openless-all/app/src/i18n/zh-CN.ts | 5 + openless-all/app/src/i18n/zh-TW.ts | 5 + .../src/pages/settings/CodingAgentSection.tsx | 72 +-------- .../src/pages/settings/ShortcutsSection.tsx | 62 ++++++++ 8 files changed, 173 insertions(+), 129 deletions(-) diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index ab901a3c..422c636b 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -1404,77 +1404,90 @@ fn coding_agent_hotkey_bridge_loop(inner: Arc, rx: mpsc::Receiver { + 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) { + 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) { 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) { }); let mut final_text = String::new(); + let mut error_msg: Option = 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) { 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) { diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 23038848..3a492ce8 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -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', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 83ee205a..cee9052e 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -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: '右側の ✓ をクリック', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 5a1f56eb..fd02d0e9 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -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: '오른쪽 ✓ 클릭', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index 7d61e926..8ea63f07 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -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: '点击右侧 ✓', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index fc0636f9..b39b6542 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -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: '點擊右側 ✓', diff --git a/openless-all/app/src/pages/settings/CodingAgentSection.tsx b/openless-all/app/src/pages/settings/CodingAgentSection.tsx index 4bf4905f..30b864fe 100644 --- a/openless-all/app/src/pages/settings/CodingAgentSection.tsx +++ b/openless-all/app/src/pages/settings/CodingAgentSection.tsx @@ -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 ( - - {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 })} @@ -106,19 +59,6 @@ export function CodingAgentSection() { - {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, - )} -