diff --git a/src-tauri/src/acp/custom_registry.rs b/src-tauri/src/acp/custom_registry.rs index fc6c71df3..4b9a62ed6 100644 --- a/src-tauri/src/acp/custom_registry.rs +++ b/src-tauri/src/acp/custom_registry.rs @@ -664,7 +664,7 @@ struct Entry { meta: &'static AcpAgentMeta, /// Display icon, kept beside the launch metadata rather than inside /// [`AcpAgentMeta`]: it is presentation, not launch data, and every one of - /// the twelve built-in metas would otherwise need a field it never uses. + /// the thirteen built-in metas would otherwise need a field it never uses. icon: Option<&'static str>, /// Whether the user declared the agent reads the shared `.agents/skills` /// store. Beside the meta for the same reason as `icon`: it drives the diff --git a/src-tauri/src/acp/file_system_runtime.rs b/src-tauri/src/acp/file_system_runtime.rs index d2fffdc92..804ca2dbd 100644 --- a/src-tauri/src/acp/file_system_runtime.rs +++ b/src-tauri/src/acp/file_system_runtime.rs @@ -541,6 +541,7 @@ fn agent_root_slots(agent_type: AgentType) -> &'static [RootSlot] { // A custom ACP agent has no codeg-known private directory layout — // codeg never reads its store (history comes from codeg's own ACP // transcript), so there is nothing to widen the sandbox roots for. + AgentType::Dsh => &[], AgentType::Custom(_) => &[], } } diff --git a/src-tauri/src/acp/registry.rs b/src-tauri/src/acp/registry.rs index 7602229a4..03d7ff959 100644 --- a/src-tauri/src/acp/registry.rs +++ b/src-tauri/src/acp/registry.rs @@ -138,7 +138,7 @@ pub fn current_platform() -> &'static str { } } -/// The twelve built-in agents. Excludes user-registered custom agents — use +/// The thirteen built-in agents. Excludes user-registered custom agents — use /// [`all_acp_agents`] for the live set. pub fn builtin_acp_agents() -> Vec { vec![ @@ -154,10 +154,11 @@ pub fn builtin_acp_agents() -> Vec { AgentType::Pi, AgentType::Grok, AgentType::Cursor, + AgentType::Dsh, ] } -/// Every agent codeg can currently drive: the twelve built-ins followed by the +/// Every agent codeg can currently drive: the thirteen built-ins followed by the /// user's registered custom ACP agents (sorted by id). pub fn all_acp_agents() -> Vec { let mut agents = builtin_acp_agents(); @@ -179,6 +180,7 @@ pub fn registry_id_for(agent_type: AgentType) -> &'static str { AgentType::Pi => "pi-acp", AgentType::Grok => "grok-build", AgentType::Cursor => "cursor", + AgentType::Dsh => "dsh-acp", // A custom agent's registry id IS its identity. AgentType::Custom(id) => id, } @@ -198,6 +200,7 @@ pub fn from_registry_id(id: &str) -> Option { "pi-acp" => Some(AgentType::Pi), "grok-build" => Some(AgentType::Grok), "cursor" => Some(AgentType::Cursor), + "dsh-acp" => Some(AgentType::Dsh), // Only ids the user has actually registered resolve. An unregistered // id must stay `None` so the ACP-registry picker still offers it as // "addable" rather than treating it as already supported. @@ -744,6 +747,22 @@ pub fn get_agent_meta(agent_type: AgentType) -> AcpAgentMeta { }), }, }, + AgentType::Dsh => AcpAgentMeta { + agent_type, + // DSH's automation-only ACP bridge rejects mcpServers, same as + // OpenClaw; forwarding any entry makes session/new fail. + supports_mcp: false, + name: "DeepSeek Harness", + description: "DeepSeek Harness coding agent (ACP, automation-only)", + distribution: AgentDistribution::Npx { + version: "0.1.1", + package: "@asteroida/dsh-codeg-adapter@0.1.1", + cmd: "dsh-codeg", + args: &[], + env: &[], + node_required: Some("20.0.0"), + }, + }, // Handled by the early return above; kept so the match stays // exhaustive without a catch-all that could swallow a new built-in. AgentType::Custom(_) => unreachable!("custom agents resolve via custom_registry"), @@ -929,6 +948,12 @@ mod tests { "hermes-agent@0.20.0", Some("20.0.0"), ); + assert_npx_version( + AgentType::Dsh, + "0.1.1", + "@asteroida/dsh-codeg-adapter@0.1.1", + Some("20.0.0"), + ); } // The Hermes launch command must be the wrapper's `hermes` bin with the @@ -986,12 +1011,12 @@ mod tests { // (`CustomAgentDef::supports_mcp`), so a registry hydrated by another test // may legitimately hold an opted-out one. #[test] - fn only_builtin_openclaw_opts_out_of_mcp() { + fn only_openclaw_and_dsh_opt_out_of_mcp() { for agent_type in builtin_acp_agents() { let meta = get_agent_meta(agent_type); assert_eq!( meta.supports_mcp, - agent_type != AgentType::OpenClaw, + agent_type != AgentType::OpenClaw && agent_type != AgentType::Dsh, "unexpected supports_mcp for {agent_type:?}" ); } diff --git a/src-tauri/src/acp/remote_registry.rs b/src-tauri/src/acp/remote_registry.rs index 19c39a8a6..13a2dbd82 100644 --- a/src-tauri/src/acp/remote_registry.rs +++ b/src-tauri/src/acp/remote_registry.rs @@ -271,7 +271,7 @@ pub async fn fetch_catalog( /// id is `kimi-code`. const BUILTIN_REGISTRY_ALIASES: &[&str] = &["kimi"]; -/// Whether an id is one of codeg's twelve hand-written agents. Deliberately +/// Whether an id is one of codeg's thirteen hand-written agents. Deliberately /// does NOT consult the custom registry (unlike `registry::from_registry_id`, /// which resolves registered custom ids too) — the picker needs "codeg ships /// this natively", not "codeg can currently launch this". diff --git a/src-tauri/src/commands/acp.rs b/src-tauri/src/commands/acp.rs index 77774684d..8f949f589 100644 --- a/src-tauri/src/commands/acp.rs +++ b/src-tauri/src/commands/acp.rs @@ -6996,6 +6996,9 @@ pub(crate) fn skill_storage_spec(agent_type: AgentType) -> Option None, // codeg cannot detect where an arbitrary ACP agent loads skills from, // so custom agents are gated on the user's own declaration: that the // agent reads the shared `.agents/skills` store (the cross-agent @@ -8241,6 +8244,10 @@ fn cascade_update_agent_config( // (`acpUpdateAgentEnv`); it does not write provider creds into // ~/.grok/config.toml and does not participate in the cascade. } + AgentType::Dsh => { + // DSH reads DEEPSEEK_API_KEY from the environment (no on-disk + // config file), so there is nothing to cascade into a file. + } AgentType::Custom(_) => { // Custom agents are deliberately configuration-free: codeg writes // no config file for them and they are excluded from the diff --git a/src-tauri/src/commands/conversations.rs b/src-tauri/src/commands/conversations.rs index 72112a914..76ffb14c5 100644 --- a/src-tauri/src/commands/conversations.rs +++ b/src-tauri/src/commands/conversations.rs @@ -295,6 +295,7 @@ pub async fn get_conversation( AgentType::Pi => Box::new(PiParser::new()), AgentType::Grok => Box::new(GrokParser::new()), AgentType::Cursor => Box::new(CursorParser::new()), + AgentType::Dsh => Box::new(AcpNativeParser::new(agent_type)), // Custom ACP agents have no native store to reverse-engineer; // their history is codeg's own ACP transcript. AgentType::Custom(_) => Box::new(AcpNativeParser::new(agent_type)), @@ -1037,6 +1038,7 @@ pub async fn get_folder_conversation_core( AgentType::Pi => Box::new(PiParser::new()), AgentType::Grok => Box::new(GrokParser::new()), AgentType::Cursor => Box::new(CursorParser::new()), + AgentType::Dsh => Box::new(AcpNativeParser::new(at)), AgentType::Custom(_) => Box::new(AcpNativeParser::new(at)), }; match parser.get_conversation(&eid) { diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index 80930bc50..68602fbe2 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -2459,6 +2459,9 @@ pub fn read_servers_for_agent_type( // pi-acp drops ACP-wire MCP and pi has no native MCP (it needs a // third-party extension), so codeg manages no MCP servers for pi (v1). AgentType::Pi => Ok(BTreeMap::new()), + // DSH's automation-only ACP bridge rejects mcpServers; codeg manages + // no MCP servers for it, and it has no native MCP config to read. + AgentType::Dsh => Ok(BTreeMap::new()), // Custom agents get MCP purely over the ACP wire (`session/new`'s // `mcpServers`); codeg deliberately knows nothing about their native // config files, so there is no per-agent store to read back here. diff --git a/src-tauri/src/db/service/agent_setting_service.rs b/src-tauri/src/db/service/agent_setting_service.rs index 528ab1c11..ff0d9ff0e 100644 --- a/src-tauri/src/db/service/agent_setting_service.rs +++ b/src-tauri/src/db/service/agent_setting_service.rs @@ -40,6 +40,7 @@ fn default_enabled(agent_type: AgentType) -> bool { | AgentType::Pi | AgentType::Grok | AgentType::Cursor + | AgentType::Dsh // A user who just registered a custom agent wants to use it. | AgentType::Custom(_) ) @@ -228,3 +229,27 @@ fn is_sqlite_full_error(err: &DbError) -> bool { let message = err.to_string(); message.contains("database or disk is full") || message.contains("(code: 13)") } + +#[cfg(test)] +mod tests { + use super::*; + + /// Every built-in must be enabled by default. A newly added built-in + /// without a `default_enabled` arm silently lands disabled in settings, + /// and the launch gate then rejects it at connect time — this test turns + /// that into a compile-visible failure instead of a runtime surprise. + #[test] + fn every_builtin_agent_is_enabled_by_default() { + for agent_type in crate::acp::registry::builtin_acp_agents() { + assert!( + default_enabled(agent_type), + "built-in {agent_type:?} must be enabled by default" + ); + } + } + + #[test] + fn custom_agents_are_enabled_by_default() { + assert!(default_enabled(AgentType::custom("goose").unwrap())); + } +} diff --git a/src-tauri/src/db/service/import_service.rs b/src-tauri/src/db/service/import_service.rs index 809ad81e3..0584848d2 100644 --- a/src-tauri/src/db/service/import_service.rs +++ b/src-tauri/src/db/service/import_service.rs @@ -51,6 +51,7 @@ fn build_parser(agent_type: AgentType) -> Box { AgentType::Pi => Box::new(PiParser::new()), AgentType::Grok => Box::new(GrokParser::new()), AgentType::Cursor => Box::new(CursorParser::new()), + AgentType::Dsh => Box::new(crate::parsers::acp_native::AcpNativeParser::new(agent_type)), // Custom agents' history lives in codeg's own ACP transcript. AgentType::Custom(_) => Box::new(crate::parsers::acp_native::AcpNativeParser::new( agent_type, @@ -103,7 +104,7 @@ where let mut seen: std::collections::HashSet<(AgentType, String)> = std::collections::HashSet::new(); let mut done = 0u32; - // Awaiting in parser order only affects callback ordering — all twelve + // Awaiting in parser order only affects callback ordering — all thirteen // walks already run concurrently on the blocking pool. for (at, task) in tasks { let mut count = 0u32; diff --git a/src-tauri/src/models/agent.rs b/src-tauri/src/models/agent.rs index d12f39d79..ddb2bbcd1 100644 --- a/src-tauri/src/models/agent.rs +++ b/src-tauri/src/models/agent.rs @@ -8,7 +8,7 @@ pub const CUSTOM_AGENT_WIRE_PREFIX: &str = "custom:"; /// Which agent backs a conversation. /// -/// The twelve named variants are compile-time built-ins with hand-written +/// The thirteen named variants are compile-time built-ins with hand-written /// launch metadata (`acp::registry`) and a dedicated transcript parser /// (`parsers::*`). [`AgentType::Custom`] is the open end: a user-registered /// ACP agent whose launch metadata lives in the database @@ -33,12 +33,13 @@ pub enum AgentType { Pi, Grok, Cursor, + Dsh, /// A user-registered ACP agent, identified by its ACP-registry id /// (interned). Ordered last so built-ins keep their relative order. Custom(&'static str), } -/// The twelve compile-time agents, in declaration order. Does NOT include +/// The thirteen compile-time agents, in declaration order. Does NOT include /// custom agents — use [`crate::acp::registry::all_acp_agents`] for the live /// set that includes them. pub const BUILTIN_AGENT_TYPES: &[AgentType] = &[ @@ -54,6 +55,7 @@ pub const BUILTIN_AGENT_TYPES: &[AgentType] = &[ AgentType::Pi, AgentType::Grok, AgentType::Cursor, + AgentType::Dsh, ]; impl AgentType { @@ -98,6 +100,7 @@ impl AgentType { AgentType::Pi => Cow::Borrowed("pi"), AgentType::Grok => Cow::Borrowed("grok"), AgentType::Cursor => Cow::Borrowed("cursor"), + AgentType::Dsh => Cow::Borrowed("dsh"), AgentType::Custom(id) => Cow::Owned(format!("{CUSTOM_AGENT_WIRE_PREFIX}{id}")), } } @@ -118,6 +121,7 @@ impl AgentType { "pi" => Some(AgentType::Pi), "grok" => Some(AgentType::Grok), "cursor" => Some(AgentType::Cursor), + "dsh" => Some(AgentType::Dsh), other => other .strip_prefix(CUSTOM_AGENT_WIRE_PREFIX) .and_then(AgentType::custom), @@ -157,6 +161,7 @@ pub fn is_valid_custom_agent_id(id: &str) -> bool { | "pi" | "grok" | "cursor" + | "dsh" ) } @@ -189,6 +194,7 @@ impl fmt::Display for AgentType { AgentType::Pi => write!(f, "Pi"), AgentType::Grok => write!(f, "Grok"), AgentType::Cursor => write!(f, "Cursor"), + AgentType::Dsh => write!(f, "DeepSeek Harness"), // Prefer the registered display name; fall back to the raw id when // the registry has not been hydrated (or the agent was deleted // while conversations still reference it). @@ -221,6 +227,7 @@ mod tests { (AgentType::Pi, "pi"), (AgentType::Grok, "grok"), (AgentType::Cursor, "cursor"), + (AgentType::Dsh, "dsh"), ]; for (agent, wire) in expected { assert_eq!(agent.as_wire(), wire); @@ -288,6 +295,7 @@ mod tests { // Shadowing a built-in would make `from_wire` ambiguous. "codex", "claude_code", + "dsh", ] { assert!( !is_valid_custom_agent_id(bad), diff --git a/src/components/agent-icon.tsx b/src/components/agent-icon.tsx index b2c72f015..83141f7b6 100644 --- a/src/components/agent-icon.tsx +++ b/src/components/agent-icon.tsx @@ -389,6 +389,26 @@ const CursorMonoIcon = memo(function CursorMonoIcon({ ) }) +const DshColorIcon = memo(function DshColorIcon({ size = "1em" }: IconProps) { + // Official DeepSeek whale mark (deepseek-harness website/public/favicon.svg). + return ( + + DeepSeek Harness + + + ) +}) + // eslint-disable-next-line @typescript-eslint/no-explicit-any type AnyIcon = React.ComponentType @@ -399,6 +419,7 @@ const COLOR_ICONS: Partial> = { open_claw: OpenClawColorIcon, kimi_code: KimiCodeColorIcon, pi: PiColorIcon, + dsh: DshColorIcon, } const MONO_ICONS: Partial> = { diff --git a/src/lib/types.ts b/src/lib/types.ts index 107c5afe3..f282b3587 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,4 +1,4 @@ -/** The twelve agents codeg ships hand-written support for. */ +/** The thirteen agents codeg ships hand-written support for. */ export type BuiltinAgentType = | "claude_code" | "codex" @@ -12,6 +12,7 @@ export type BuiltinAgentType = | "pi" | "grok" | "cursor" + | "dsh" /** * Which agent backs a conversation. @@ -694,6 +695,7 @@ export const AGENT_DISPLAY_ORDER: BuiltinAgentType[] = [ "pi", "grok", "cursor", + "dsh", ] const AGENT_DISPLAY_ORDER_INDEX = new Map( @@ -725,6 +727,7 @@ export const ALL_AGENT_TYPES: BuiltinAgentType[] = [ "pi", "grok", "cursor", + "dsh", ] export const MODEL_PROVIDER_AGENT_TYPES: BuiltinAgentType[] = [ @@ -1033,6 +1036,7 @@ export const AGENT_LABELS: Record = { pi: "Pi", grok: "Grok", cursor: "Cursor", + dsh: "DeepSeek Harness", } export const AGENT_COLORS: Record = { @@ -1048,6 +1052,7 @@ export const AGENT_COLORS: Record = { pi: "bg-[#0D9488]", grok: "bg-neutral-900", cursor: "bg-zinc-800", + dsh: "bg-[#4D6BFE]", } // ACP connection status (matches Rust ConnectionStatus)