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
27 changes: 25 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ resolver = "2"
members = [
"crates/kernel",
"crates/core",
"crates/plugins",
"crates/tui",
"crates/server",
"apps/desktop/src-host",
Expand All @@ -32,6 +33,7 @@ tracing = "0.1"
# Local crates
codetwo-kernel = { path = "crates/kernel" }
codetwo-core = { path = "crates/core" }
codetwo-plugins = { path = "crates/plugins" }

[patch.crates-io]
# 0.2.1 links Ghostty's Windows static archive under the Unix library name. Keep the published
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ whole turn, and only then send it to the agent you choose.
- **Git-aware execution.** Use per-session worktrees, automatic checkpoints, diffs, revert, and
explicit commit/push flows.
- **Three surfaces.** C2 ships an Electrobun desktop app, a ratatui TUI, and a paired remote web
client. All three use the same Rust core and Plugin Kernel; Electrobun is the desktop shell and
relays one command/event protocol to its bundled Rust host.
client. All three compose the same Rust Core through the same plugin runtime; Electrobun is the
desktop shell and relays one command/event protocol to its bundled Rust host.

## How it fits together

Expand All @@ -50,7 +50,9 @@ Claude Code · Codex · Grok · Cursor · OpenCode 1 · OpenCode 2 · Pi · Kimi
ACP over stdio
Rust core + plugin kernel
Rust product core
Plugin composition layer
┌─────────┼─────────┐
│ │ │
Desktop TUI Remote
Expand Down Expand Up @@ -150,7 +152,8 @@ tailnet; C2 does not provide a hosted relay.
| Path | Purpose |
| -------------------------------- | --------------------------------------------------------------------------- |
| [`crates/kernel`](crates/kernel) | Reactive plugin runtime and command registry |
| [`crates/core`](crates/core) | ACP engine, sessions, providers, memory, git, terminal, browser, and skills |
| [`crates/core`](crates/core) | Plugin-independent product domain: ACP, sessions, providers, policy, and persistence |
| [`crates/plugins`](crates/plugins) | Core adapters, built-in runtime graph, extension bundles, protocol, and marketplace |
| [`crates/tui`](crates/tui) | ratatui frontend |
| [`crates/server`](crates/server) | Headless server, pairing, WebSocket protocol, and remote client |
| [`apps/desktop`](apps/desktop) | Electrobun + React + BlockNote desktop app |
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src-host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "C2 native sidecar — a JSON-lines bridge over codetwo-core."
description = "C2 native sidecar — a JSON-lines desktop container over CoreApp."

[dependencies]
serde.workspace = true
Expand All @@ -14,6 +14,7 @@ uuid.workspace = true
blake3 = "1"
codetwo-core.workspace = true
codetwo-kernel.workspace = true
codetwo-plugins.workspace = true
codetwo-server = { path = "../../../crates/server" }
futures-util = "0.3"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] }
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-host/src/automation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use std::sync::Arc;
use std::time::Duration;

use codetwo_core::app::{EngineService, EventBus, StoreService};
use codetwo_core::permission::ExecutionPolicy;
use codetwo_core::session::SessionRunState;
use codetwo_core::skill::DocBlock;
Expand All @@ -18,6 +17,7 @@ use codetwo_core::{
use codetwo_kernel::{
async_trait, Context, Injection, Plugin, PluginError, PluginResult, WeakContext,
};
use codetwo_plugins::{EngineService, EventBus, StoreService};
use serde::Deserialize;
use serde_json::Value;
use tokio::sync::broadcast;
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-host/src/device_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;

use codetwo_core::app::StoreService;
use codetwo_core::device_sync::{
device_sync_snapshot_version, merge_device_sync_documents, DeviceSyncCounts, DeviceSyncDocument,
};
use codetwo_core::session::now_millis;
use codetwo_core::Store;
use codetwo_kernel::{async_trait, Context, Injection, Plugin, PluginError, PluginResult, Service};
use codetwo_plugins::StoreService;
use futures_util::future::join_all;
use reqwest::{Client, Response, Url};
use serde::de::DeserializeOwned;
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src-host/src/host_events.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
//! Scope-owned forwarding from core broadcasts to the desktop host protocol.

use codetwo_core::app::events::PluginsChanged;
use codetwo_core::app::{EventBus, TerminalEvent, TerminalOutputEvent};
use codetwo_kernel::{
async_trait, CommandRealm, Context, Injection, Plugin, PluginError, PluginResult,
};
use codetwo_plugins::events::PluginsChanged;
use codetwo_plugins::{EventBus, TerminalEvent, TerminalOutputEvent};
use serde::Serialize;
use serde_json::Value;
use tokio::sync::broadcast;
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src-host/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ mod scene_mcp;
use std::path::PathBuf;
use std::sync::Arc;

use codetwo_core::app::plugins::{EngineInputs, EnginePlugin};
use codetwo_core::app::{AppConfig, CoreApp};
use codetwo_core::{CanvasFeatureGate, DesktopMcpConfig, Engine};
use codetwo_kernel::{
PluginCategory, PluginEntry, PluginMetadata, PluginOrigin, PluginRole, PluginScopeSupport,
};
use codetwo_plugins::builtins::{EngineInputs, EnginePlugin};
use codetwo_plugins::{AppConfig, CoreApp};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
Expand Down Expand Up @@ -152,7 +152,7 @@ pub async fn run() -> Result<(), String> {
browser_enabled: false,
};

let mut registry = codetwo_core::app::plugins::builtin_registry();
let mut registry = codetwo_plugins::builtins::builtin_registry();
#[cfg(unix)]
let engine_metadata = registry
.get("engine")
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src-host/src/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};

use codetwo_core::app::events::PluginsChanged;
use codetwo_core::app::Paths;
use codetwo_kernel::{async_trait, Context, Injection, Plugin, PluginError, PluginResult};
use codetwo_plugins::events::PluginsChanged;
use codetwo_plugins::Paths;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
Expand Down Expand Up @@ -87,7 +87,7 @@ fn lsp_start(
state.ensure_open()?;
let plugins_dir = paths.plugins();
if plugins_dir.is_dir() {
let plugins = codetwo_core::plugin::load_dir(&plugins_dir).unwrap_or_default();
let plugins = codetwo_plugins::bundle::load_dir(&plugins_dir).unwrap_or_default();
for plugin in plugins
.into_iter()
.filter(|plugin| plugin.enabled && plugin.trusted)
Expand Down
16 changes: 7 additions & 9 deletions apps/desktop/src-host/src/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use codetwo_core::app::{CanvasService, EngineService, EventBus, StoreService};
use codetwo_core::{Engine, Event, Store};
use codetwo_kernel::{async_trait, Context, Injection, Plugin, PluginError, PluginResult};
use codetwo_plugins::{CanvasService, EngineService, EventBus, StoreService};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::broadcast;
Expand Down Expand Up @@ -230,8 +230,7 @@ impl Plugin for RemotePlugin {
}

fn inject(&self) -> Injection {
Injection::required(["engine", "store", "bus", "canvas"])
.with_optional(["device-sync"])
Injection::required(["engine", "store", "bus", "canvas"]).with_optional(["device-sync"])
}

fn description(&self) -> Option<&str> {
Expand Down Expand Up @@ -295,9 +294,10 @@ impl Plugin for RemotePlugin {
let auth = Arc::new(codetwo_server::AuthState::load(Some(
service.auth_path.clone(),
)));
let device_sync_http = service.device_sync.clone().map(|device_sync| {
device_sync as Arc<dyn codetwo_server::DeviceSyncHttp>
});
let device_sync_http = service
.device_sync
.clone()
.map(|device_sync| device_sync as Arc<dyn codetwo_server::DeviceSyncHttp>);
let bound = codetwo_server::bind_and_serve_with_services(
service.engine.clone(),
service.events.clone(),
Expand Down Expand Up @@ -481,9 +481,7 @@ impl Plugin for RemotePlugin {
true
} else {
match &service.device_sync {
Some(sync) => sync
.revoke_device(&args.id)
.map_err(PluginError::new)?,
Some(sync) => sync.revoke_device(&args.id).map_err(PluginError::new)?,
None => false,
}
};
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-host/src/scene_mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::io::{BufRead, BufReader, Write};

use codetwo_core::app::{CoreApp, SceneService, StoreService};
use codetwo_plugins::{CoreApp, SceneService, StoreService};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

Expand Down
93 changes: 60 additions & 33 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ import { RemoteModal } from "./remote/Remote";
import { IssuesModal } from "./issues/Issues";
import { PreviewModal } from "./editor/Preview";
import { FileBrowserModal } from "./files/FileBrowser";
import { FileDockContent } from "./files/FileDockContent";
import { WorkspaceSearchModal } from "./files/WorkspaceSearch";
import type { FileRevealTarget } from "./files/FileViewer";
import { dirtyKey, isDirty as isFileDirty, markDirty } from "./files/dirty";
Expand Down Expand Up @@ -380,6 +381,10 @@ import {
type DockSurface,
type DockTab,
} from "./dock/Dock";
import { BrowserPanel } from "./browser/Browser";
import { GitDockContent } from "./git/GitDockContent";
import { TerminalDockContent } from "./terminal/TerminalDockContent";
import { TrajectoryView } from "./session/TrajectoryView";
import { SessionRail } from "./sidebar/SessionRail";
import { EnvironmentPopover } from "./environment/EnvironmentPopover";
import { MissionControlDialog } from "./sidebar/MissionControl.tsx";
Expand Down Expand Up @@ -7768,12 +7773,11 @@ export default function App() {
<div className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
{/* Also a window drag region: the overlay title bar draws nothing to grab. Buttons and
other children stay clickable — only elements carrying the attribute start a drag. */}
{/* The shared titlebar height centres the 28px controls on the same 48px line as the rail
and dock headers. With the rail collapsed, the inset clears
the traffic lights and the expand button takes the wordmark's place. */}
{/* The shared 40px title line keeps every pane on one baseline. With the rail collapsed,
the inset clears the traffic lights and the expand button takes the wordmark's place. */}
<header
className={cn(
"session-header electrobun-webkit-app-region-drag flex min-w-0 shrink-0 items-center gap-2 border-b py-2.5 pr-4",
"session-header window-titlebar electrobun-webkit-app-region-drag flex min-w-0 shrink-0 items-center gap-2 pr-4",
displayedRailCollapsed ? "window-controls-safe-main" : "pl-4",
)}
>
Expand Down Expand Up @@ -8293,36 +8297,59 @@ export default function App() {
}}
onClose={() => manualDockTab(null)}
autoTab={dockAutoHint?.surface ?? null}
highlightFile={dockAutoHint?.file ?? null}
cwd={cwd || null}
projectPath={
activeProject ? normalizePluginProjectPath(activeProject) : null
}
sessionKey={activeSession ?? "main"}
git={git}
onRefreshGit={refreshGit}
onOpenSourceControl={openSourceControl}
browserUrl={browserUrl}
onNavigate={setBrowserUrl}
onAnnotate={(n) => void annotate(n)}
onInsertFile={(p) => insertFileRef.current?.(p)}
onSendText={(text) => insertTextRef.current?.(text)}
onOpenFile={openFileTab}
openFiles={openFiles}
activeFile={activeFile}
fileReveal={fileReveal}
onActiveFile={(path) => {
setActiveFile(path);
setFileReveal(null);
content={{
trajectory: (
<TrajectoryView
turns={turns}
usage={focusedSessionUsage}
hasEarlier={focusedTranscriptState.nextBefore !== null}
loadingEarlier={focusedTranscriptState.loadingEarlier}
onLoadEarlier={() => void loadEarlierTranscript(paneLayout.focused)}
/>
),
browser: (
<BrowserPanel
url={browserUrl}
projectPath={lspProjectPath}
visible={dockTab !== null}
onNavigate={setBrowserUrl}
onAnnotate={(notes) => void annotate(notes)}
/>
),
terminal: (
<TerminalDockContent
cwd={cwd || null}
projectPath={lspProjectPath}
sessionKey={activeSession ?? "main"}
onSendText={(text) => insertTextRef.current?.(text)}
/>
),
files: (
<FileDockContent
cwd={cwd || null}
openFiles={openFiles}
activeFile={activeFile}
reveal={fileReveal}
highlightFile={dockAutoHint?.file ?? null}
onActiveFile={(path) => {
setActiveFile(path);
setFileReveal(null);
}}
onCloseFile={closeFileTab}
onInsertFile={(path) => insertFileRef.current?.(path)}
onOpenFile={openFileTab}
onSendText={(text) => insertTextRef.current?.(text)}
/>
),
git: (
<GitDockContent
cwd={cwd || null}
status={git}
onRefresh={refreshGit}
onOpenSourceControl={openSourceControl}
/>
),
}}
onCloseFile={closeFileTab}
turns={turns}
usage={sessionUsage}
hasEarlier={focusedTranscriptState.nextBefore !== null}
loadingEarlier={focusedTranscriptState.loadingEarlier}
onLoadEarlier={() =>
void loadEarlierTranscript(paneLayout.focused)
}
width={dockWidth}
onWidth={setDockWidth}
reservedWidth={railInlineWidth}
Expand Down
Loading