Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ Agent Theme is a standalone desktop app (Tauri v2). The agent (Codex Desktop / A
|---|---|---|
| **Codex Desktop** | ✅ Supported | overrides Tailwind v4 `--color-token-*` design tokens + per-module frosted glass |
| **Antigravity** | ✅ Supported | overrides shadcn / `--vscode-*` semantic tokens + per-panel frosted glass + chat-prose / code-block recolouring |
| **Linear** | ✅ Supported | rides Linear's **Dark mode**: wallpaper layer + content surfaces made translucent + frosted glass on sidebar / tabs / cards / menus / dialogs |

> Both are injected at the CDP runtime and share `theme.json`'s colour knobs; support for more agents will be added over time.
> All three are injected at the CDP runtime and share `theme.json`'s colour knobs; support for more agents will be added over time.

> [!IMPORTANT]
> **Linear must be set to Dark mode first** (Settings → Preferences → Interface theme → **Dark**). Linear's colours are driven by three systems (StyleX atomic vars + legacy `--color-*` + hardcoded literals), so light mode can't be cleanly retheme'd (text stays invisible); in Dark mode Linear natively renders dark surfaces + light text, and the theme only layers a wallpaper + frosted glass on top — crisp and flicker-free.

## Theme Showcase

Expand All @@ -38,6 +42,10 @@ Every theme is **colour-matched individually** to its own background — glass t
|---|---|
| ![Changli](docs/antigravity/changli.jpg) | ![Frost](docs/antigravity/frost.jpg) |

And the actual look on Linear (Dark mode + Changli — sidebar / title / content text blurred for privacy):

![Linear · Changli](docs/linear/changli.jpg)

**11** built-in themes (backgrounds are the respective character artworks — see [Disclaimer](#disclaimer)):

| ID | English | ID | English |
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ Agent Theme 是一个独立的桌面应用(Tauri v2)。代理(Codex Desktop / An
|---|---|---|
| **Codex Desktop** | ✅ 已适配 | 覆盖 Tailwind v4 `--color-token-*` 设计令牌 + 各模块磨砂玻璃 |
| **Antigravity** | ✅ 已适配 | 覆盖 shadcn / `--vscode-*` 语义令牌 + 各面板磨砂玻璃 + 对话正文 / 代码块重配色 |
| **Linear** | ✅ 已适配 | 基于 Linear **暗色模式**叠壁纸 + 内容区透明化 + 侧栏/标签/卡片/菜单/弹窗磨砂玻璃 |

> 两者都通过 CDP 运行时注入、共用 `theme.json` 的配色旋钮;后续适配更多代理会陆续加入。
> 三者都通过 CDP 运行时注入、共用 `theme.json` 的配色旋钮;后续适配更多代理会陆续加入。

> [!IMPORTANT]
> **Linear 需先设为暗色模式**(Settings → Preferences → Interface theme → **Dark**)。Linear 的颜色由 StyleX 原子变量 + 旧 `--color-*` + 硬编码字面色三套系统驱动,浅色模式无法干净覆盖文字;暗色模式下 Linear 原生渲染暗底浅字,换肤只在其上叠壁纸与磨砂玻璃,清晰不闪。

## 主题展示

Expand All @@ -38,6 +42,10 @@ Agent Theme 是一个独立的桌面应用(Tauri v2)。代理(Codex Desktop / An
|---|---|
| ![Changli](docs/antigravity/changli.jpg) | ![Frost](docs/antigravity/frost.jpg) |

Linear 上的实际效果(暗色模式 + 长离 Changli,侧栏 / 标题 / 内容文字已做模糊处理):

![Linear · Changli](docs/linear/changli.jpg)

内置 **11 套**主题(背景图为各自角色美术,详见[免责声明](#免责声明)):

| ID | 中文名 | English | ID | 中文名 | English |
Expand Down
65 changes: 47 additions & 18 deletions src-tauri/src/cdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ pub fn find_main_target<'a>(targets: &'a [Target], kind: &AgentKind) -> Option<&
.iter()
.find(|t| t.target_type == "page" && t.title.contains("Antigravity"))
}
AgentKind::Linear => {
// Thin Electron shell that loads the remote web app directly
// (renderer/index.html is empty; main process loads
// https://linear.app/auth/desktop → https://linear.app).
if let Some(main) = targets
.iter()
.find(|t| t.url.starts_with("https://linear.app") && t.target_type == "page")
{
return Some(main);
}
targets
.iter()
.find(|t| t.target_type == "page" && t.title.contains("Linear"))
}
}
}

Expand Down Expand Up @@ -200,6 +214,27 @@ pub async fn clear_theme(
Ok(())
}

pub async fn reload_page(port: u16, kind: &AgentKind) -> Result<(), String> {
let targets = list_targets(port).await?;
let target =
find_main_target(&targets, kind).ok_or("Could not find Agent main window target")?;

let ws_url = target
.web_socket_debugger_url
.as_ref()
.ok_or("Target has no WebSocket URL")?;
let (mut ws_stream, _): (WebSocketStream<MaybeTlsStream<TcpStream>>, _) =
connect_async(ws_url.as_str())
.await
.map_err(|e| format!("WebSocket connect failed: {}", e))?;

make_cdp_request(&mut ws_stream, "Page.enable", serde_json::json!({})).await?;
make_cdp_request(&mut ws_stream, "Page.reload", serde_json::json!({})).await?;

let _ = ws_stream.close(None).await;
Ok(())
}

#[cfg(test)]
mod tests {
use super::{find_main_target, Target};
Expand Down Expand Up @@ -235,25 +270,19 @@ mod tests {

assert_eq!(target.map(|t| t.url.as_str()), Some("app://-/index.html"));
}
}

pub async fn reload_page(port: u16, kind: &AgentKind) -> Result<(), String> {
let targets = list_targets(port).await?;
let target =
find_main_target(&targets, kind).ok_or("Could not find Agent main window target")?;

let ws_url = target
.web_socket_debugger_url
.as_ref()
.ok_or("Target has no WebSocket URL")?;
let (mut ws_stream, _): (WebSocketStream<MaybeTlsStream<TcpStream>>, _) =
connect_async(ws_url.as_str())
.await
.map_err(|e| format!("WebSocket connect failed: {}", e))?;
#[test]
fn finds_linear_remote_page() {
let targets = vec![page(
"MOC-130 issue",
"https://linear.app/mochance/issue/MOC-130",
)];

make_cdp_request(&mut ws_stream, "Page.enable", serde_json::json!({})).await?;
make_cdp_request(&mut ws_stream, "Page.reload", serde_json::json!({})).await?;
let target = find_main_target(&targets, &AgentKind::Linear);

let _ = ws_stream.close(None).await;
Ok(())
assert_eq!(
target.map(|t| t.url.as_str()),
Some("https://linear.app/mochance/issue/MOC-130")
);
}
}
9 changes: 9 additions & 0 deletions src-tauri/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ pub enum AgentKind {
#[default]
Codex,
Antigravity,
Linear,
}

impl fmt::Display for AgentKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AgentKind::Codex => write!(f, "Codex"),
AgentKind::Antigravity => write!(f, "Antigravity"),
AgentKind::Linear => write!(f, "Linear"),
}
}
}
Expand All @@ -27,13 +29,15 @@ impl AgentKind {
match self {
AgentKind::Codex => "Codex",
AgentKind::Antigravity => "Antigravity",
AgentKind::Linear => "Linear",
}
}

pub fn display_name_en(&self) -> &str {
match self {
AgentKind::Codex => "Codex",
AgentKind::Antigravity => "Antigravity",
AgentKind::Linear => "Linear",
}
}

Expand All @@ -42,20 +46,23 @@ impl AgentKind {
match self {
AgentKind::Codex => "Codex",
AgentKind::Antigravity => "Antigravity",
AgentKind::Linear => "Linear",
}
}

pub fn app_bundle_path(&self) -> &'static str {
match self {
AgentKind::Codex => "/Applications/Codex.app",
AgentKind::Antigravity => "/Applications/Antigravity.app",
AgentKind::Linear => "/Applications/Linear.app",
}
}

pub fn executable_path(&self) -> &'static str {
match self {
AgentKind::Codex => "/Applications/Codex.app/Contents/MacOS/Codex",
AgentKind::Antigravity => "/Applications/Antigravity.app/Contents/MacOS/Antigravity",
AgentKind::Linear => "/Applications/Linear.app/Contents/MacOS/Linear",
}
}

Expand All @@ -64,6 +71,7 @@ impl AgentKind {
match self {
AgentKind::Codex => vec!["Codex"],
AgentKind::Antigravity => vec!["Antigravity"],
AgentKind::Linear => vec!["Linear"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Process name pattern "Linear" causes false-positive process detection

The process_name_patterns() for AgentKind::Linear returns vec!["Linear"], which is a common English word substring. In agent.rs:27, is_agent_process_running uses pname.contains(p) with an OR against the exe path check (agent.rs:35), so any process whose name contains "Linear" (e.g. "LinearMouse" — a popular macOS mouse utility) would make the function return true even when the actual Linear app isn't running. Unlike "Codex" or "Antigravity" which are distinctive names, "Linear" is a common substring. This causes get_agent_status (lib.rs:81) to report "running": true when Linear isn't actually running, confusing users with a "Running" + "No debug port" status. The kill/restart flow is safe (it uses is_agent_bundle_process_running which checks the full exe path), so this is limited to misleading UI status.

Prompt for agents
The process_name_patterns() for Linear returns vec!["Linear"], but "Linear" is a common substring that matches other macOS apps like LinearMouse. The is_agent_process_running function in agent.rs:20-39 uses contains() with an OR condition (name_match || exe_match), so any process with "Linear" in its name triggers a false positive.

Two possible fixes:
1. Make the name pattern more specific, e.g. use an exact match instead of contains(). Change agent.rs:27 to use pname == p or pname.starts_with(p) rather than pname.contains(p). However, this would also affect Codex/Antigravity matching.
2. Change the logic in is_agent_process_running to require BOTH name_match AND exe_match for the Linear agent (or use AND instead of OR globally — but that might break detection for cases where the exe path isn't available).
3. Another option: for Linear specifically, only rely on binary_path_patterns (exe_match) and not process_name_patterns. This could be done by returning an empty vec for process_name_patterns for Linear, or by refining the detection logic to require exe_match when available.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
}

Expand All @@ -72,6 +80,7 @@ impl AgentKind {
match self {
AgentKind::Codex => vec!["/Applications/Codex.app/"],
AgentKind::Antigravity => vec!["/Applications/Antigravity.app/"],
AgentKind::Linear => vec!["/Applications/Linear.app/"],
}
}
}
Expand Down
Loading
Loading