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
40 changes: 40 additions & 0 deletions docs/agents/zcode.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,46 @@ Images and embedded resources follow the bridge's negotiated ACP capabilities. P

The bridge accepts up to 100 content blocks and 50 MiB of decoded content per prompt, with a 10 MiB limit per image. Image attachments must be PNG, JPEG, WebP, GIF, BMP or SVG. Other image formats, invalid base64 and oversized prompts return an error. Pause any native goal loop before sending binary attachments; queued goal prompts accept only text.

## Skills

Codeg's Skills settings can list, read, create, edit, delete and import ZCode
directory bundles (`<id>/SKILL.md`). ZCode also appears in the custom, expert,
science, Office and UI Render skill installation matrices.

Codeg uses the folders the official ZCode runtime loads skills from. Global
skills live in `~/.zcode/skills/` and then the shared `~/.agents/skills/`, both
in your home directory. New global skills and Codeg installation links go into
`~/.zcode/skills/`, which keeps them separate from the shared store. Flat `.md`
files are not ZCode skill bundles.

Project skills are created in `<session cwd>/.zcode/skills/`, where ZCode's
own settings create them. Codeg also lists and edits the other project skills
ZCode loads: those in `<session cwd>/.agents/skills/` and, when the session cwd
is inside a Git repository, those in both folders of every parent folder up to
the repository root. A skill from a parent folder is edited where it is. When
several of these folders contain the same skill folder, Codeg lists and edits
only the first: the one nearest the session cwd, and within one folder the
`.zcode` copy. ZCode ranks these folders in the same order, but it picks a skill
by the `name` in its `SKILL.md`, and prefers a global skill with the same name.
Of these project folders, the bridge's command list includes only
`<session cwd>/.agents/skills/`; ZCode loads skills from all of them.

`ZCODE_HOME` does not move these folders. The runtime's global skill folders
always hang off the home directory, and its project folders off the session
cwd. The bridge's command list reads `$ZCODE_HOME/skills/` instead of
`~/.zcode/skills/`. With `ZCODE_HOME` pointing elsewhere, it can therefore
offer commands for skills the runtime cannot load, and leave out skills Codeg
installed. Extra folders added with ZCode's `skills.roots` setting are not
listed. Codeg finds the home directory the way ZCode's Node runtime does, from
`HOME`, or `USERPROFILE` on Windows, in Codeg's own process environment. It does
not read the agent's environment overrides.

Codeg manages user-owned files and installation links; the bridge continues to
decide which skills appear as commands. Native per-path disable flags in
`cli/config.json` still apply. Codeg does not rewrite those flags or edit
plugin caches. Reconnect the conversation after changing skills so the bridge
publishes its current commands.

## Sessions and tools

A new session explicitly confirms Build mode before sending its first prompt. A valid saved mode, including Yolo, takes precedence. Confirming the mode makes the bridge create the ZCode backend session when the conversation connects rather than at its first prompt. Existing sessions use the normal restore and preference flow. Model and thought selectors follow ACP configuration updates rather than a hardcoded model list.
Expand Down
618 changes: 584 additions & 34 deletions src-tauri/src/commands/acp.rs

Large diffs are not rendered by default.

101 changes: 101 additions & 0 deletions src-tauri/src/commands/custom_skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,30 @@ mod tests {
assert!(agents.iter().all(|a| skill_storage_spec(*a).is_some()));
}

/// ZCode's runtime loads user skills from `<os home>/.zcode/skills` whatever
/// `ZCODE_HOME` says, so a matrix link must land there and not follow it.
/// Its home is the one Node reads, `HOME` or on Windows `USERPROFILE`;
/// both are pinned so the link lands in the fixture on every platform.
#[test]
fn zcode_joins_the_custom_skill_matrix_at_its_os_home_store() {
let tmp = tempfile::tempdir().unwrap();
let (home, relocated) = (tmp.path().join("home"), tmp.path().join("relocated"));
temp_env::with_vars(
[
("HOME", Some(&home)),
("USERPROFILE", Some(&home)),
("ZCODE_HOME", Some(&relocated)),
],
|| {
assert!(supported_agents().contains(&AgentType::ZCode));
assert_eq!(
agent_link_path(AgentType::ZCode, "example").unwrap(),
home.join(".zcode").join("skills").join("example")
);
},
);
}

#[test]
fn a_custom_agent_joins_the_matrix_by_declaring_a_store() {
use crate::acp::custom_registry::{
Expand Down Expand Up @@ -1106,6 +1130,83 @@ mod tests {
.await;
}

#[cfg(unix)]
#[tokio::test]
async fn fs_zcode_import_and_matrix_links_use_its_os_home_store() {
let tmp = tempfile::tempdir().unwrap();
// The runtime's user store; a relocated `ZCODE_HOME` must not move it.
let native = tmp.path().join(".zcode");
let relocated = tmp.path().join("relocated");
temp_env::async_with_vars(
[
("HOME", Some(tmp.path())),
("ZCODE_HOME", Some(relocated.as_path())),
],
async {
assert!(central_experts_dir().starts_with(tmp.path()));
let source = native.join("skills/zcode-import-fixture");
fs::create_dir_all(source.join("references")).unwrap();
fs::write(
source.join("SKILL.md"),
"---\nname: Original\ndescription: Example\n---\nbody",
)
.unwrap();
fs::write(source.join("references/proof.txt"), "asset").unwrap();
let imported =
custom_import_from_agent(AgentType::ZCode, vec!["zcode-import-fixture".into()])
.await
.unwrap();
assert!(imported[0].ok, "{imported:?}");
assert_eq!(
fs::read_to_string(
central_experts_dir().join("zcode-import-fixture/references/proof.txt")
)
.unwrap(),
"asset"
);

let id = "zcode-managed-fixture";
custom_create_skill(
id.into(),
"---\nname: Managed\ndescription: Test\n---\nbody".into(),
)
.await
.unwrap();
let rows = custom_apply_links(vec![LinkOp {
expert_id: id.into(),
agent_type: AgentType::ZCode,
enable: true,
}])
.await
.unwrap();
assert!(rows[0].ok, "{rows:?}");
assert_eq!(
rows[0].status.as_ref().unwrap().state,
ExpertLinkState::LinkedToCodeg
);
let link = native.join("skills").join(id);
assert!(path_is_symlink(&link));
let rows = custom_list_all_install_statuses().await.unwrap();
assert!(rows.iter().any(|r| r.expert_id == id
&& r.agent_type == AgentType::ZCode
&& r.state == ExpertLinkState::LinkedToCodeg));
let rows = custom_apply_links(vec![LinkOp {
expert_id: id.into(),
agent_type: AgentType::ZCode,
enable: false,
}])
.await
.unwrap();
assert!(rows[0].ok, "{rows:?}");
assert!(!link.exists() && !path_is_symlink(&link));
assert!(central_experts_dir().join(id).join("SKILL.md").is_file());
assert!(source.join("references/proof.txt").is_file());
assert!(!relocated.exists(), "nothing is written under ZCODE_HOME");
},
)
.await;
}

/// The generative-UI skill shares the central store but is only Codeg's
/// once its marker is there; until then a `ui-render` is the user's.
#[cfg(unix)]
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/commands/experts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,7 @@ fn supported_agents() -> Vec<AgentType> {
AgentType::Qoder,
AgentType::Antigravity,
AgentType::Kiro,
AgentType::ZCode,
AgentType::Trae,
AgentType::CommandCode,
];
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/commands/office_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,7 @@ fn supported_agents() -> Vec<AgentType> {
AgentType::Qoder,
AgentType::Antigravity,
AgentType::Kiro,
AgentType::ZCode,
AgentType::Trae,
AgentType::CommandCode,
];
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/commands/science.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ fn supported_agents() -> Vec<AgentType> {
AgentType::Qoder,
AgentType::Antigravity,
AgentType::Kiro,
AgentType::ZCode,
AgentType::Trae,
AgentType::CommandCode,
];
Expand Down
40 changes: 35 additions & 5 deletions src-tauri/src/web/handlers/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use crate::acp::error::AcpError;
use crate::acp::opencode_plugins::PluginCheckSummary;
use crate::acp::preflight::PreflightResult;
use crate::acp::types::{
AcpAgentInfo, AcpAgentStatus, AgentDiagnosticsReport, AgentSkillContent, AgentSkillLayout,
AgentSkillScope, AgentSkillsListResult, ConnectionInfo, ForkResultInfo,
AcpAgentInfo, AcpAgentStatus, AgentDiagnosticsReport, AgentSkillContent, AgentSkillItem,
AgentSkillLayout, AgentSkillScope, AgentSkillsListResult, ConnectionInfo, ForkResultInfo,
};
use crate::app_error::{AppCommandError, AppErrorCode};
use crate::app_state::AppState;
Expand Down Expand Up @@ -294,8 +294,8 @@ pub struct AcpSaveAgentSkillParams {

pub async fn acp_save_agent_skill(
Json(params): Json<AcpSaveAgentSkillParams>,
) -> Result<Json<()>, AppCommandError> {
acp_commands::acp_save_agent_skill(
) -> Result<Json<AgentSkillItem>, AppCommandError> {
let skill = acp_commands::acp_save_agent_skill(
params.agent_type,
params.scope,
params.skill_id,
Expand All @@ -305,7 +305,7 @@ pub async fn acp_save_agent_skill(
)
.await
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
Ok(Json(()))
Ok(Json(skill))
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -1513,6 +1513,36 @@ mod tests {

use crate::acp::agent_mentions::append_agent_routes;

/// The editor reopens what it just saved from this response, so the web
/// endpoint must return the saved item as the desktop command does.
#[tokio::test]
async fn skill_save_returns_the_item_the_web_editor_needs() {
let tmp = tempfile::tempdir().unwrap();
let params = serde_json::from_value(serde_json::json!({
"agentType": "zcode",
"scope": "project",
"skillId": "web-skill",
"content": "---\nname: web-skill\ndescription: Test\n---\nbody",
"workspacePath": tmp.path().to_string_lossy(),
"layout": "markdown_file"
}))
.unwrap();
let response = acp_save_agent_skill(Json(params)).await.unwrap();
let value = serde_json::to_value(response.0).unwrap();
assert_eq!(value["id"], "web-skill");
assert_eq!(value["scope"], "project");
assert_eq!(value["layout"], "skill_directory");
assert_eq!(value["description"], "Test");
let directory = tmp.path().join(".zcode/skills/web-skill");
assert_eq!(
std::path::Path::new(value["path"].as_str().unwrap()),
directory
);
assert!(std::fs::read_to_string(directory.join("SKILL.md"))
.unwrap()
.ends_with("body"));
}

#[test]
fn prompt_params_deserialize_camel_case_fields() {
let params: AcpPromptParams = serde_json::from_value(serde_json::json!({
Expand Down
6 changes: 3 additions & 3 deletions src/components/settings/acp-agent-settings.zcode.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ describe("ZCode settings panel", () => {
agent_type: "zcode",
name: "ZCode",
registry_id: "codeg-zcode-acp",
registry_version: "0.69.2",
installed_version: "0.69.2",
registry_version: "0.76.2",
installed_version: "0.76.2",
description: "Official ZCode runtime via the community ACP bridge",
skills_capable: false,
skills_capable: true,
supports_custom_version: true,
available: true,
distribution_type: "npx",
Expand Down
Loading
Loading