Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
333ba3d
feat(coding-agent): 快速 Agent 引擎 + Claude 控制台(无头 Claude Code)
Jun 4, 2026
d5756db
Merge remote-tracking branch 'origin/beta' into feat/fast-agent
Jun 5, 2026
d8dc546
feat(coding-agent): 语音 Agent 设置 UI(开关 + Claude/OpenCode 选择 + 双热键 + 模型…
Jun 5, 2026
0463c30
feat(coding-agent): 接线快取用执行链路(热键 → 选中 → 无头 Claude → 回插)
Jun 5, 2026
8211dea
feat(coding-agent): 拆分两功能 — 选中润色(快取用键)+ Cloud Agent(语音键,占位);热键移到通用设置
Jun 5, 2026
b532ef9
fix: 快速 Agent 热键自愈 + 统一停用旋钮布局
Jun 5, 2026
51d1587
test: 自愈逻辑抽成纯函数 sanitize_agent_hotkeys + 4 个单测
Jun 5, 2026
1b2a15d
test: 加胶囊内省 seam + 集成测试证明「按下面板键→弹可见胶囊」
Jun 5, 2026
6c8e284
fix: Cloud Agent 占位提示改为中性色(不再是红色报错)
Jun 5, 2026
ee50665
feat: 长按听写键唤起 Cloud Agent 语音;移除 ⌘⇧J/⌘⇧Enter 组合键
Jun 5, 2026
e9e7ba5
fix: 长按阈值从会话开始计时,扣除 ASR 握手耗时
Jun 5, 2026
ff918ec
feat: 发给无头 Claude 的需求统一包一层「一次性完成」自动化前置说明
Jun 5, 2026
0082e0b
feat(less-computer): 专用语音键(Codex 实现,已审核)
Jun 5, 2026
dae966f
feat(less-computer): 「按住说话键」挪到 通用→快捷键,去掉高级里的重复
Jun 5, 2026
8fd3ca4
fix(less-computer): Agent 真正动手干活——放行+护栏(acceptEdits + Bash/工具 + 高风险拦截)
Jun 5, 2026
4aa293e
Merge remote-tracking branch 'origin/beta' into feat/fast-agent
Jun 5, 2026
5ee5cb1
feat(less-computer): 处理态文案 thinking 改为 using(Agent 操控电脑时);并入 origin/b…
Jun 5, 2026
5546e50
feat(settings): 模型改抽拉下拉(Haiku/Sonnet/Opus,不用记ID)+ Cloud 控制台默认折叠
Jun 5, 2026
f735ae9
chore(less-computer): 精简文案 desc 与 voiceHotkeyDesc 缩短 5 语言
Jun 5, 2026
d6ca97d
feat(less-computer): streaming chat popup window + inline approval sc…
Jun 5, 2026
26e7c48
fix(less-computer): 把 less-computer 窗口加进 capabilities,否则 webview 收不到事…
Jun 5, 2026
93c54ca
feat(less-computer): 连续对话(--continue 续会话+多轮气泡) + 彩虹跑马灯描边/流动呼吸背景
Jun 5, 2026
cf933cb
fix(less-computer): 长按松手停录音(latch不再被5s轮询重置) + 全屏弧形彩虹描边浮层(点击穿透/无缝) + 流…
Jun 5, 2026
120cc15
fix(less-computer): 全屏描边贴边修复(逻辑坐标+菜单栏层级)+苹果式高斯柔光(细带blur内化)+按下即亮; 弹框可拖…
Jun 5, 2026
8c1e4df
fix(less-computer): polish agent capsule UX
Jun 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion openless-all/app/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ int openless_common_controls_v6_manifest_dependency_anchor = 0;
cc::Build::new()
.file(&source_path)
.compile("openless_common_controls_v6_manifest_dependency");
println!("cargo:rustc-link-arg=/INCLUDE:openless_common_controls_v6_manifest_dependency_anchor");
println!(
"cargo:rustc-link-arg=/INCLUDE:openless_common_controls_v6_manifest_dependency_anchor"
);
}

/// 编译 vendored Open-Less/qwen-asr 的 C 源(仅 macOS)。
Expand Down
2 changes: 1 addition & 1 deletion openless-all/app/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capabilities for OpenLess windows",
"windows": ["main", "capsule", "qa"],
"windows": ["main", "capsule", "qa", "less-computer", "less-computer-glow"],
"permissions": [
"core:default",
"core:window:default",
Expand Down
5 changes: 4 additions & 1 deletion openless-all/app/src-tauri/src/asr/local/cache.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#![cfg_attr(target_os = "linux", allow(dead_code, unused_imports, unused_variables))]
#![cfg_attr(
target_os = "linux",
allow(dead_code, unused_imports, unused_variables)
)]
//! 本地 Qwen3-ASR 引擎缓存。
//!
//! 用途:避免每次 dictation 都重加载 1.2GB+ 模型。引擎一次 load 后驻留在内存,
Expand Down
2 changes: 1 addition & 1 deletion openless-all/app/src-tauri/src/asr/local/test_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ use std::time::Instant;
use anyhow::Result;
use serde::Serialize;

use super::models::ModelId;
#[cfg(target_os = "macos")]
use super::models::model_dir;
use super::models::ModelId;

/// 内嵌测试音频。原始文件 `vendor/qwen-asr/samples/test_speech.wav`
/// 内容:"Hello. This is a test of the Voxtrail speech-to-text system."
Expand Down
4 changes: 3 additions & 1 deletion openless-all/app/src-tauri/src/asr/volcengine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ impl VolcengineStreamingASR {
.map_err(|e| VolcengineASRError::ConnectionFailed(e.to_string()))?,
);

let (ws, _resp) = connect_async(request).await.map_err(classify_connect_error)?;
let (ws, _resp) = connect_async(request)
.await
.map_err(classify_connect_error)?;
let (write, read) = ws.split();

let (tx, rx) = oneshot::channel();
Expand Down
5 changes: 2 additions & 3 deletions openless-all/app/src-tauri/src/asr/whisper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,8 @@ fn extract_confident_text(json: &serde_json::Value) -> String {
.and_then(|v| v.as_f64())
.unwrap_or(1.0);

let is_hallucination = (no_speech > 0.6 && avg_logprob < -0.5)
|| compression > 2.4
|| avg_logprob < -1.0;
let is_hallucination =
(no_speech > 0.6 && avg_logprob < -0.5) || compression > 2.4 || avg_logprob < -1.0;
if is_hallucination {
log::warn!(
"[whisper] 丢弃疑似幻听段落: no_speech={:.2} avg_logprob={:.2} compression={:.2} text={:?}",
Expand Down
233 changes: 233 additions & 0 deletions openless-all/app/src-tauri/src/coding_agent/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
//! 无头 Claude Code(`claude -p`)调用参数构造。
//!
//! 纯逻辑:把一个 [`CodingAgentRequest`] 翻译成 `claude` 的命令行参数列表。
//! prompt 本身**不**进 argv(避免出现在进程列表里泄露),由运行器写进 stdin。

use std::path::PathBuf;

/// Claude Code 权限模式,对应 CLI `--permission-mode` 的取值(已对本机 v2.1.161 核实)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CodingAgentPermissionMode {
/// 只读/计划模式:不改文件。
Plan,
/// 默认:每个动作都要确认(无头下等于大多被拒,少用)。
Default,
/// 放行可恢复的编辑/动作(本项目「放行 + 护栏」的默认)。
AcceptEdits,
/// 跳过所有权限检查——仅高级区,绝不做默认。
BypassPermissions,
}

impl CodingAgentPermissionMode {
/// 传给 `--permission-mode` 的字符串。
pub fn as_cli_arg(self) -> &'static str {
match self {
Self::Plan => "plan",
Self::Default => "default",
Self::AcceptEdits => "acceptEdits",
Self::BypassPermissions => "bypassPermissions",
}
}
}

impl Default for CodingAgentPermissionMode {
fn default() -> Self {
Self::AcceptEdits
}
}

/// 一次无头 agent 运行的完整请求。
#[derive(Debug, Clone)]
pub struct CodingAgentRequest {
/// 会话标识,用于丢弃迟到事件。
pub session_id: String,
/// 最终发给 Claude 的指令(写入 stdin,不进 argv)。
pub prompt: String,
/// 工作目录;同时作为 `--add-dir` 限定文件作用域。
pub cwd: Option<PathBuf>,
pub model: Option<String>,
pub fallback_model: Option<String>,
pub permission_mode: CodingAgentPermissionMode,
pub allowed_tools: Vec<String>,
pub disallowed_tools: Vec<String>,
/// 单次运行成本硬上限(`--max-budget-usd`)。
pub max_budget_usd: Option<f64>,
/// 运行超时(秒)。
pub timeout_secs: u64,
/// 额外系统提示词(`--append-system-prompt`)。
pub extra_system_prompt: Option<String>,
/// 护栏 settings JSON 文件路径(`--settings`)。
pub settings_json_path: Option<PathBuf>,
/// 是否保留会话(false 时加 `--no-session-persistence`,快取用走 false 更快)。
pub session_persistence: bool,
/// 续接最近一次会话(`--continue`)。Less Computer 连续对话用:第二轮起带上下文。
pub continue_session: bool,
}

impl CodingAgentRequest {
/// 最小化构造:只给会话 id 和 prompt,其余取保守默认。
pub fn new(session_id: impl Into<String>, prompt: impl Into<String>) -> Self {
Self {
session_id: session_id.into(),
prompt: prompt.into(),
cwd: None,
model: None,
fallback_model: None,
permission_mode: CodingAgentPermissionMode::default(),
allowed_tools: Vec::new(),
disallowed_tools: Vec::new(),
max_budget_usd: None,
timeout_secs: 300,
extra_system_prompt: None,
settings_json_path: None,
session_persistence: true,
continue_session: false,
}
}
}

/// 构造 `claude` 的命令行参数(不含可执行文件本身,也不含 prompt)。
///
/// 固定使用无头流式:`-p --output-format stream-json --verbose --include-partial-messages`,
/// 这样前端能拿到逐字 delta。
pub fn build_claude_args(req: &CodingAgentRequest) -> Vec<String> {
let mut args: Vec<String> = vec![
"-p".into(),
"--output-format".into(),
"stream-json".into(),
"--verbose".into(),
"--include-partial-messages".into(),
"--permission-mode".into(),
req.permission_mode.as_cli_arg().into(),
];

if let Some(model) = &req.model {
args.push("--model".into());
args.push(model.clone());
}
if let Some(fm) = &req.fallback_model {
args.push("--fallback-model".into());
args.push(fm.clone());
}
if let Some(cwd) = &req.cwd {
args.push("--add-dir".into());
args.push(cwd.to_string_lossy().into_owned());
}
if !req.allowed_tools.is_empty() {
args.push("--allowedTools".into());
args.push(req.allowed_tools.join(","));
}
if !req.disallowed_tools.is_empty() {
args.push("--disallowedTools".into());
args.push(req.disallowed_tools.join(","));
}
if let Some(budget) = req.max_budget_usd {
args.push("--max-budget-usd".into());
args.push(format!("{budget}"));
}
if let Some(path) = &req.settings_json_path {
args.push("--settings".into());
args.push(path.to_string_lossy().into_owned());
}
if let Some(sp) = &req.extra_system_prompt {
args.push("--append-system-prompt".into());
args.push(sp.clone());
}
if !req.session_persistence {
args.push("--no-session-persistence".into());
}
if req.continue_session {
args.push("--continue".into());
}

args
}

#[cfg(test)]
mod tests {
use super::*;

fn arg_value<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
args.iter()
.position(|a| a == flag)
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str())
}

#[test]
fn default_args_are_headless_streaming() {
let req = CodingAgentRequest::new("s1", "hello");
let args = build_claude_args(&req);
assert!(args.contains(&"-p".to_string()));
assert_eq!(arg_value(&args, "--output-format"), Some("stream-json"));
assert!(args.contains(&"--verbose".to_string()));
assert!(args.contains(&"--include-partial-messages".to_string()));
// prompt 不能出现在 argv 里
assert!(!args.iter().any(|a| a.contains("hello")));
}

#[test]
fn permission_mode_maps_to_cli_string() {
assert_eq!(CodingAgentPermissionMode::Plan.as_cli_arg(), "plan");
assert_eq!(
CodingAgentPermissionMode::AcceptEdits.as_cli_arg(),
"acceptEdits"
);
assert_eq!(
CodingAgentPermissionMode::BypassPermissions.as_cli_arg(),
"bypassPermissions"
);
let mut req = CodingAgentRequest::new("s", "p");
req.permission_mode = CodingAgentPermissionMode::Plan;
assert_eq!(
arg_value(&build_claude_args(&req), "--permission-mode"),
Some("plan")
);
}

#[test]
fn default_permission_mode_is_accept_edits() {
assert_eq!(
CodingAgentPermissionMode::default(),
CodingAgentPermissionMode::AcceptEdits
);
}

#[test]
fn optional_flags_are_emitted_when_set() {
let mut req = CodingAgentRequest::new("s", "p");
req.model = Some("sonnet".into());
req.fallback_model = Some("haiku".into());
req.max_budget_usd = Some(0.5);
req.cwd = Some(PathBuf::from("/tmp/work"));
req.allowed_tools = vec!["Bash(git *)".into(), "Edit".into()];
req.disallowed_tools = vec!["Bash(rm -rf:*)".into()];
req.settings_json_path = Some(PathBuf::from("/tmp/guard.json"));
req.extra_system_prompt = Some("be terse".into());
req.session_persistence = false;

let args = build_claude_args(&req);
assert_eq!(arg_value(&args, "--model"), Some("sonnet"));
assert_eq!(arg_value(&args, "--fallback-model"), Some("haiku"));
assert_eq!(arg_value(&args, "--max-budget-usd"), Some("0.5"));
assert_eq!(arg_value(&args, "--add-dir"), Some("/tmp/work"));
assert_eq!(arg_value(&args, "--allowedTools"), Some("Bash(git *),Edit"));
assert_eq!(
arg_value(&args, "--disallowedTools"),
Some("Bash(rm -rf:*)")
);
assert_eq!(arg_value(&args, "--settings"), Some("/tmp/guard.json"));
assert_eq!(arg_value(&args, "--append-system-prompt"), Some("be terse"));
assert!(args.contains(&"--no-session-persistence".to_string()));
}

#[test]
fn optional_flags_absent_by_default() {
let req = CodingAgentRequest::new("s", "p");
let args = build_claude_args(&req);
assert!(arg_value(&args, "--model").is_none());
assert!(arg_value(&args, "--max-budget-usd").is_none());
assert!(!args.contains(&"--no-session-persistence".to_string()));
}
}
Loading