diff --git a/package.json b/package.json
index f613c069e..6b36f37b6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "codeg",
"private": true,
- "version": "0.21.3",
+ "version": "0.21.4",
"packageManager": "pnpm@11.9.0",
"scripts": {
"dev": "next dev --turbopack",
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 3e4b6afe9..29da54ec9 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -1014,7 +1014,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
name = "codeg"
-version = "0.21.3"
+version = "0.21.4"
dependencies = [
"aes-gcm",
"agent-client-protocol-schema",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 1671af3fc..6430a627e 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "codeg"
-version = "0.21.3"
+version = "0.21.4"
description = "Agent Code Generation App"
authors = ["feitao"]
edition = "2021"
diff --git a/src-tauri/src/acp/registry.rs b/src-tauri/src/acp/registry.rs
index 89c53668b..f2a5c9df0 100644
--- a/src-tauri/src/acp/registry.rs
+++ b/src-tauri/src/acp/registry.rs
@@ -33,7 +33,7 @@ pub enum AgentDistribution {
/// Used for ACP agents distributed as Python packages (e.g. Hermes).
Uvx {
version: &'static str,
- /// The `uvx --from` package spec, e.g. "hermes-agent[acp,mcp]==0.18.2".
+ /// The `uvx --from` package spec, e.g. "hermes-agent[acp,mcp]==0.19.0".
package: &'static str,
/// The console-script entry point to run, e.g. "hermes-acp".
cmd: &'static str,
@@ -194,8 +194,8 @@ pub fn get_agent_meta(agent_type: AgentType) -> AcpAgentMeta {
name: "Claude Code",
description: "ACP wrapper for Anthropic's Claude",
distribution: AgentDistribution::Npx {
- version: "0.58.1",
- package: "@agentclientprotocol/claude-agent-acp@0.58.1",
+ version: "0.60.0",
+ package: "@agentclientprotocol/claude-agent-acp@0.60.0",
cmd: "claude-agent-acp",
args: &[],
env: &[],
@@ -231,8 +231,8 @@ pub fn get_agent_meta(agent_type: AgentType) -> AcpAgentMeta {
name: "Gemini CLI",
description: "Google's official CLI for Gemini",
distribution: AgentDistribution::Npx {
- version: "0.50.0",
- package: "@google/gemini-cli@0.50.0",
+ version: "0.51.0",
+ package: "@google/gemini-cli@0.51.0",
cmd: "gemini",
args: &["--acp", "--skip-trust"],
env: &[],
@@ -261,8 +261,8 @@ pub fn get_agent_meta(agent_type: AgentType) -> AcpAgentMeta {
name: "Cline",
description: "Autonomous coding agent CLI",
distribution: AgentDistribution::Npx {
- version: "3.0.44",
- package: "cline@3.0.44",
+ version: "3.0.46",
+ package: "cline@3.0.46",
cmd: "cline",
args: &["--acp"],
env: &[],
@@ -275,34 +275,34 @@ pub fn get_agent_meta(agent_type: AgentType) -> AcpAgentMeta {
name: "OpenCode",
description: "The open source coding agent",
distribution: AgentDistribution::Binary {
- version: "1.18.3",
+ version: "1.18.4",
cmd: "opencode",
args: &["acp"],
env: &[],
platforms: &[
PlatformBinary {
platform: "darwin-aarch64",
- url: "https://github.com/anomalyco/opencode/releases/download/v1.18.3/opencode-darwin-arm64.zip",
+ url: "https://github.com/anomalyco/opencode/releases/download/v1.18.4/opencode-darwin-arm64.zip",
},
PlatformBinary {
platform: "darwin-x86_64",
- url: "https://github.com/anomalyco/opencode/releases/download/v1.18.3/opencode-darwin-x64.zip",
+ url: "https://github.com/anomalyco/opencode/releases/download/v1.18.4/opencode-darwin-x64.zip",
},
PlatformBinary {
platform: "linux-aarch64",
- url: "https://github.com/anomalyco/opencode/releases/download/v1.18.3/opencode-linux-arm64.tar.gz",
+ url: "https://github.com/anomalyco/opencode/releases/download/v1.18.4/opencode-linux-arm64.tar.gz",
},
PlatformBinary {
platform: "linux-x86_64",
- url: "https://github.com/anomalyco/opencode/releases/download/v1.18.3/opencode-linux-x64.tar.gz",
+ url: "https://github.com/anomalyco/opencode/releases/download/v1.18.4/opencode-linux-x64.tar.gz",
},
PlatformBinary {
platform: "windows-aarch64",
- url: "https://github.com/anomalyco/opencode/releases/download/v1.18.3/opencode-windows-arm64.zip",
+ url: "https://github.com/anomalyco/opencode/releases/download/v1.18.4/opencode-windows-arm64.zip",
},
PlatformBinary {
platform: "windows-x86_64",
- url: "https://github.com/anomalyco/opencode/releases/download/v1.18.3/opencode-windows-x64.zip",
+ url: "https://github.com/anomalyco/opencode/releases/download/v1.18.4/opencode-windows-x64.zip",
},
],
dir_entry: None,
@@ -314,13 +314,13 @@ pub fn get_agent_meta(agent_type: AgentType) -> AcpAgentMeta {
name: "Hermes Agent",
description: "Nous Research's self-improving agent (ACP via uvx)",
distribution: AgentDistribution::Uvx {
- version: "0.18.2",
- package: "hermes-agent[acp,mcp]==0.18.2",
+ version: "0.19.0",
+ package: "hermes-agent[acp,mcp]==0.19.0",
cmd: "hermes-acp",
args: &[],
env: &[],
uv_required: Some("0.5.0"),
- // hermes-agent 0.18.2 is `requires-python = ">=3.11,<3.14"`, and
+ // hermes-agent 0.19.0 is `requires-python = ">=3.11,<3.14"`, and
// its win32 dep `pywinpty` (>=2.0.0,<3) has no Python 3.14 wheel
// (the 2.0.15 source build fails against PyO3's 3.13 ceiling).
// Without this pin uvx grabs the machine's default interpreter
@@ -336,8 +336,8 @@ pub fn get_agent_meta(agent_type: AgentType) -> AcpAgentMeta {
name: "CodeBuddy",
description: "Tencent Cloud's official AI coding assistant (ACP)",
distribution: AgentDistribution::Npx {
- version: "2.124.0",
- package: "@tencent-ai/codebuddy-code@2.124.0",
+ version: "2.125.0",
+ package: "@tencent-ai/codebuddy-code@2.125.0",
cmd: "codebuddy",
args: &["--acp"],
env: &[],
@@ -350,8 +350,8 @@ pub fn get_agent_meta(agent_type: AgentType) -> AcpAgentMeta {
name: "Kimi Code",
description: "Moonshot AI's official CLI coding assistant (ACP)",
distribution: AgentDistribution::Npx {
- version: "0.27.0",
- package: "@moonshot-ai/kimi-code@0.27.0",
+ version: "0.28.1",
+ package: "@moonshot-ai/kimi-code@0.28.1",
cmd: "kimi",
args: &["acp"],
env: &[],
@@ -603,14 +603,14 @@ mod tests {
fn registry_pins_current_acp_agent_versions() {
assert_npx_version(
AgentType::ClaudeCode,
- "0.58.1",
- "@agentclientprotocol/claude-agent-acp@0.58.1",
+ "0.60.0",
+ "@agentclientprotocol/claude-agent-acp@0.60.0",
Some("22.0.0"),
);
assert_npx_version(
AgentType::Gemini,
- "0.50.0",
- "@google/gemini-cli@0.50.0",
+ "0.51.0",
+ "@google/gemini-cli@0.51.0",
Some("20.0.0"),
);
assert_npx_version(
@@ -621,20 +621,20 @@ mod tests {
);
assert_npx_version(
AgentType::Cline,
- "3.0.44",
- "cline@3.0.44",
+ "3.0.46",
+ "cline@3.0.46",
Some("22.0.0"),
);
assert_npx_version(
AgentType::CodeBuddy,
- "2.124.0",
- "@tencent-ai/codebuddy-code@2.124.0",
+ "2.125.0",
+ "@tencent-ai/codebuddy-code@2.125.0",
Some("22.0.0"),
);
assert_npx_version(
AgentType::KimiCode,
- "0.27.0",
- "@moonshot-ai/kimi-code@0.27.0",
+ "0.28.1",
+ "@moonshot-ai/kimi-code@0.28.1",
Some("22.19.0"),
);
assert_npx_version(
@@ -650,13 +650,13 @@ mod tests {
"@xai-official/grok@0.2.103",
Some("20.0.0"),
);
- assert_binary_version(AgentType::OpenCode, "1.18.3", "/releases/download/v1.18.3/");
+ assert_binary_version(AgentType::OpenCode, "1.18.4", "/releases/download/v1.18.4/");
assert_uvx_version(
AgentType::Hermes,
- "0.18.2",
- "hermes-agent[acp,mcp]==0.18.2",
+ "0.19.0",
+ "hermes-agent[acp,mcp]==0.19.0",
Some("0.5.0"),
- // hermes-agent 0.18.2 is requires-python `<3.14`; uvx must pin an
+ // hermes-agent 0.19.0 is requires-python `<3.14`; uvx must pin an
// interpreter it (and its win32 `pywinpty` dep) supports.
Some("3.13"),
);
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index cb9f5df53..312858466 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "codeg",
- "version": "0.21.3",
+ "version": "0.21.4",
"identifier": "app.codeg",
"build": {
"beforeDevCommand": "pnpm tauri:before-dev",
diff --git a/src/app/globals.css b/src/app/globals.css
index 2d47f6512..e5b8da238 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -2023,7 +2023,9 @@
/* Browser-style tab separators (embedded desktop title-bar tab strips).
Each tab draws a hairline on its LEFT edge; the line is suppressed for the
first tab and around any active / hovered tab (Chrome behavior) using only
- adjacent-sibling selectors — no :has(), so it is robust across webviews. */
+ adjacent-sibling selectors — no :has(), so it is robust across webviews.
+ Tone: `--border` off-image; boosted to `--ws-chrome-border` under a workspace
+ background so it matches the tab arch / strip-line border (rule below). */
.browser-tab-item {
position: relative;
}
@@ -2058,9 +2060,10 @@
the flush-pinned button wrapper begins, has none. Same hairline geometry as
`.browser-tab-item::before` (the wrapper shares the tabs' flex box, so the
`calc(50% - 3px)` nudge centers it on the strip midline identically) and the
- same `var(--border)` tone, so it reads as just another inter-tab separator.
- Suppressed when the last tab is active or hovered, mirroring how a tab drops
- the separators flanking the active/hovered tab. */
+ same tone (`--border` off-image, `--ws-chrome-border` on — see below), so it
+ reads as just another inter-tab separator. Suppressed when the last tab is
+ active or hovered, mirroring how a tab drops the separators flanking the
+ active/hovered tab. */
.tab-strip-tail::before {
content: "";
position: absolute;
@@ -2079,6 +2082,16 @@
opacity: 0;
}
+/* Under a workspace background image, lift the inter-tab separators (and the
+ trailing new-conversation divider) to the boosted `--ws-chrome-border` so they
+ match the tab's own arch / strip-line border color instead of the faint
+ theme `--border`, which washes out on the frosted strip. Off-image they stay
+ `--border` (above). Covers both the conversation and file tab strips. */
+[data-workspace-bg="on"] .browser-tab-item::before,
+[data-workspace-bg="on"] .tab-strip-tail::before {
+ background: var(--ws-chrome-border);
+}
+
/* Embedded tab label — no ellipsis: fade the right edge so an overflowing title
dissolves instead of hard-cutting with "…". Pairs with `overflow-hidden
whitespace-nowrap` on the label (replaces `truncate`).
@@ -2167,16 +2180,18 @@
detail header. A dedicated span hosts these because the tab's own surface is
`overflow-hidden` (it would clip the outboard flares) and its ::before is
already the separator hairline. It is rendered for every embedded tab but
- stays invisible (opacity 0) until the tab is active OR hovered — so hovering
- a tab reveals the same reverse corners. The fill matches the tab surface:
- solid `var(--background)` for the active tab, and the translucent hover shade
- (`bg-background/60`) for a hovered non-active tab, so the concave never pops
- as solid white. The radial-gradient mask carves the concave quarter (no color
- seam). Since the item has no horizontal padding, the seat spans the surface
- flush (`left/right: 0`) and each flare reaches 0.5rem outward; that fits
- exactly within the group's `px-2` padding, so `overflow-hidden` never clips
- the first/last tab's flare. The active tab is elevated (`z-10` on the item),
- so a hovered neighbour's flare can never paint over the active tab's corner. */
+ stays invisible (opacity 0) until the tab is ACTIVE. Only the active tab's
+ solid `var(--background)` body reaches the strip bottom, so its feet flare from
+ real corners and read cleanly. A hovered non-active tab instead shows a
+ floating inset pill (`browser-tab-hover`, inset 2/2/8/2) that stops 8px short
+ of the bottom; revealing the feet under it left two concave nubs disconnected
+ from the pill at the bottom corners, so hover no longer reveals the seat. The
+ radial-gradient mask carves the concave quarter (no color seam). Since the item
+ has no horizontal padding, the seat spans the surface flush (`left/right: 0`)
+ and each flare reaches 0.5rem outward; that fits exactly within the group's
+ `px-2` padding, so `overflow-hidden` never clips the first/last tab's flare.
+ The active tab is elevated (`z-10` on the item), so a hovered neighbour's flare
+ can never paint over the active tab's corner. */
.browser-tab-seat {
position: absolute;
bottom: 0;
@@ -2192,8 +2207,7 @@
transition: none;
}
}
-.browser-tab-item[data-active="true"] .browser-tab-seat,
-.browser-tab-item:hover .browser-tab-seat {
+.browser-tab-item[data-active="true"] .browser-tab-seat {
opacity: 1;
}
.browser-tab-seat::before,
@@ -2205,12 +2219,6 @@
width: 0.5rem;
background-color: var(--background);
}
-/* Hovered (non-active) tab: match its translucent hover fill so the concave
- corner blends with the hover surface instead of popping solid white. */
-.browser-tab-item:not([data-active="true"]):hover .browser-tab-seat::before,
-.browser-tab-item:not([data-active="true"]):hover .browser-tab-seat::after {
- background-color: color-mix(in oklab, var(--background) 60%, transparent);
-}
.browser-tab-seat::before {
left: -0.5rem;
-webkit-mask-image: radial-gradient(
@@ -2230,19 +2238,12 @@
mask-image: radial-gradient(circle at top right, transparent 7px, #000 8px);
}
/* Workspace 背景图开启:所有 tab 全透明,seat 反向圆角默认不显示(否则透明 tab 会挂
- var(--background) 白色小圆角);hover 的非激活 tab 也抑制其填充凹角,保持全透明基调。 */
+ var(--background) 白色小圆角)。非激活 tab 的 seat 已不随 hover 显示(见上「仅激活」),
+ 激活 tab 的脚在下方用真实 border 重画。 */
[data-workspace-bg="on"] .browser-tab-seat::before,
[data-workspace-bg="on"] .browser-tab-seat::after {
background-color: transparent;
}
-[data-workspace-bg="on"]
- .browser-tab-item:not([data-active="true"]):hover
- .browser-tab-seat::before,
-[data-workspace-bg="on"]
- .browser-tab-item:not([data-active="true"]):hover
- .browser-tab-seat::after {
- background-color: transparent;
-}
/* 激活 tab 的两个反向圆角脚(bg-on):用**真实 border + border-radius** 画 1px 凹弧描边,
取代旧的 radial-gradient 环带 mask——环带核仅 0.5px + 两侧抗锯齿,实机易出毛边/线头
(用户反馈「底部两角有线跑出来、过渡不自然」)。seat ::before/::after 本就是 gutter 里
diff --git a/src/components/layout/branch-dropdown.tsx b/src/components/layout/branch-dropdown.tsx
index 0f8acb903..d92a53b1c 100644
--- a/src/components/layout/branch-dropdown.tsx
+++ b/src/components/layout/branch-dropdown.tsx
@@ -812,7 +812,7 @@ export function BranchDropdown({
>
)}
- {branch ?? head?.short_sha ?? t("noBranch")}
+ {branch ?? head?.branch ?? head?.short_sha ?? t("noBranch")}
diff --git a/src/stores/app-workspace-store.test.ts b/src/stores/app-workspace-store.test.ts
index ec8e7b73e..1d99dcf4d 100644
--- a/src/stores/app-workspace-store.test.ts
+++ b/src/stores/app-workspace-store.test.ts
@@ -1,9 +1,16 @@
-import { beforeEach, describe, expect, it } from "vitest"
+import { beforeEach, describe, expect, it, vi } from "vitest"
import {
resetAppWorkspaceStore,
useAppWorkspaceStore,
} from "./app-workspace-store"
-import type { DbConversationSummary } from "@/lib/types"
+import type { DbConversationSummary, FolderDetail } from "@/lib/types"
+
+vi.mock("@/lib/api", () => ({
+ getFolder: vi.fn(),
+}))
+
+const { getFolder } = await import("@/lib/api")
+const mockGetFolder = vi.mocked(getFolder)
function makeSummary(
overrides: Partial & { id: number }
@@ -101,3 +108,47 @@ describe("updateConversationLocal — stats reference stability", () => {
expect(useAppWorkspaceStore.getState().stats?.total_messages).toBe(14)
})
})
+
+function makeFolder(
+ overrides: Partial & { id: number }
+): FolderDetail {
+ return {
+ name: "repo",
+ path: "/tmp/repo",
+ git_branch: null,
+ default_agent_type: null,
+ last_opened_at: "2026-01-01T00:00:00.000Z",
+ sort_order: 1,
+ color: "#000000",
+ parent_id: null,
+ kind: "regular",
+ alias: null,
+ ...overrides,
+ }
+}
+
+describe("refreshFolder — branch null-guard", () => {
+ it("keeps the poll-resolved branch when the refreshed row's git_branch is null", async () => {
+ // Git-head polling has populated the display branch; the folder row's
+ // `git_branch` column is null (it always is today), so the refresh must
+ // leave the polled name alone.
+ useAppWorkspaceStore.getState().setBranch(1, "feature/x")
+ mockGetFolder.mockResolvedValue(makeFolder({ id: 1, git_branch: null }))
+
+ await useAppWorkspaceStore.getState().refreshFolder(1)
+
+ // Regression guard for the "no branch" flash: a null DB branch must not
+ // clobber the polled name (which would blank the bottom selector until the
+ // next poll, up to 10s later).
+ expect(useAppWorkspaceStore.getState().branches.get(1)).toBe("feature/x")
+ })
+
+ it("adopts the refreshed branch when the row actually carries one", async () => {
+ useAppWorkspaceStore.getState().setBranch(1, "old")
+ mockGetFolder.mockResolvedValue(makeFolder({ id: 1, git_branch: "main" }))
+
+ await useAppWorkspaceStore.getState().refreshFolder(1)
+
+ expect(useAppWorkspaceStore.getState().branches.get(1)).toBe("main")
+ })
+})
diff --git a/src/stores/app-workspace-store.ts b/src/stores/app-workspace-store.ts
index 71a0e2262..00b919ad3 100644
--- a/src/stores/app-workspace-store.ts
+++ b/src/stores/app-workspace-store.ts
@@ -404,13 +404,22 @@ export const useAppWorkspaceStore = create()(
return updated
}
const { folders, allFolders, branches } = get()
- const nextBranches = new Map(branches)
- nextBranches.set(id, detail.git_branch ?? null)
- set({
+ const patch: Partial = {
folders: patchList(folders),
allFolders: patchList(allFolders),
- branches: nextBranches,
- })
+ }
+ // Only adopt the DB branch when the row actually carries one. A folder's
+ // `git_branch` column is always null today — branch state is resolved by
+ // git-head polling (`applyGitHead`) — so unconditionally writing it here
+ // would clobber the polled branch name with null and make the selector
+ // flash "no branch" until the next poll (up to 10s). Mirrors the same
+ // null-guard the `folder://changed` handler already applies.
+ if (detail.git_branch != null) {
+ const nextBranches = new Map(branches)
+ nextBranches.set(id, detail.git_branch)
+ patch.branches = nextBranches
+ }
+ set(patch)
} catch (err) {
console.error("[AppWorkspace] refreshFolder failed:", err)
}