Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src-tauri/src/acp/custom_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/acp/file_system_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(_) => &[],
}
}
Expand Down
33 changes: 29 additions & 4 deletions src-tauri/src/acp/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AgentType> {
vec![
Expand All @@ -154,10 +154,11 @@ pub fn builtin_acp_agents() -> Vec<AgentType> {
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<AgentType> {
let mut agents = builtin_acp_agents();
Expand All @@ -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,
}
Expand All @@ -198,6 +200,7 @@ pub fn from_registry_id(id: &str) -> Option<AgentType> {
"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.
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:?}"
);
}
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/acp/remote_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down
7 changes: 7 additions & 0 deletions src-tauri/src/commands/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6996,6 +6996,9 @@ pub(crate) fn skill_storage_spec(agent_type: AgentType) -> Option<SkillStorageSp
],
project_rel_dirs: vec![".cursor/skills", ".agents/skills"],
}),
// DSH loads skills through its own cordis.yml (under ~/.dsh); codeg
// does not detect or manage them, so it declares no skill storage.
AgentType::Dsh => 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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/commands/conversations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/src/commands/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 25 additions & 0 deletions src-tauri/src/db/service/agent_setting_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(_)
)
Expand Down Expand Up @@ -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()));
}
}
3 changes: 2 additions & 1 deletion src-tauri/src/db/service/import_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ fn build_parser(agent_type: AgentType) -> Box<dyn AgentParser> {
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,
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 10 additions & 2 deletions src-tauri/src/models/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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] = &[
Expand All @@ -54,6 +55,7 @@ pub const BUILTIN_AGENT_TYPES: &[AgentType] = &[
AgentType::Pi,
AgentType::Grok,
AgentType::Cursor,
AgentType::Dsh,
];

impl AgentType {
Expand Down Expand Up @@ -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}")),
}
}
Expand All @@ -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),
Expand Down Expand Up @@ -157,6 +161,7 @@ pub fn is_valid_custom_agent_id(id: &str) -> bool {
| "pi"
| "grok"
| "cursor"
| "dsh"
)
}

Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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),
Expand Down
21 changes: 21 additions & 0 deletions src/components/agent-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<svg
height={size}
style={baseSvgStyle}
viewBox="0 0 50 50"
width={size}
xmlns="http://www.w3.org/2000/svg"
>
<title>DeepSeek Harness</title>
<path
d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z"
fill="#4D6BFE"
fillRule="nonzero"
/>
</svg>
)
})

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyIcon = React.ComponentType<any>

Expand All @@ -399,6 +419,7 @@ const COLOR_ICONS: Partial<Record<AgentType, AnyIcon>> = {
open_claw: OpenClawColorIcon,
kimi_code: KimiCodeColorIcon,
pi: PiColorIcon,
dsh: DshColorIcon,
}

const MONO_ICONS: Partial<Record<AgentType, AnyIcon>> = {
Expand Down
7 changes: 6 additions & 1 deletion src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -12,6 +12,7 @@ export type BuiltinAgentType =
| "pi"
| "grok"
| "cursor"
| "dsh"

/**
* Which agent backs a conversation.
Expand Down Expand Up @@ -694,6 +695,7 @@ export const AGENT_DISPLAY_ORDER: BuiltinAgentType[] = [
"pi",
"grok",
"cursor",
"dsh",
]

const AGENT_DISPLAY_ORDER_INDEX = new Map<AgentType, number>(
Expand Down Expand Up @@ -725,6 +727,7 @@ export const ALL_AGENT_TYPES: BuiltinAgentType[] = [
"pi",
"grok",
"cursor",
"dsh",
]

export const MODEL_PROVIDER_AGENT_TYPES: BuiltinAgentType[] = [
Expand Down Expand Up @@ -1033,6 +1036,7 @@ export const AGENT_LABELS: Record<BuiltinAgentType, string> = {
pi: "Pi",
grok: "Grok",
cursor: "Cursor",
dsh: "DeepSeek Harness",
}

export const AGENT_COLORS: Record<BuiltinAgentType, string> = {
Expand All @@ -1048,6 +1052,7 @@ export const AGENT_COLORS: Record<BuiltinAgentType, string> = {
pi: "bg-[#0D9488]",
grok: "bg-neutral-900",
cursor: "bg-zinc-800",
dsh: "bg-[#4D6BFE]",
}

// ACP connection status (matches Rust ConnectionStatus)
Expand Down