Skip to content

[workspace-split] Phase 0 + 1 (partial): Workspace skeleton, leaf extractions, CI adaptations#79

Merged
Crsei merged 5 commits intoCrsei:rust-litefrom
yaohaowei0914:workspace-split
Apr 21, 2026
Merged

[workspace-split] Phase 0 + 1 (partial): Workspace skeleton, leaf extractions, CI adaptations#79
Crsei merged 5 commits intoCrsei:rust-litefrom
yaohaowei0914:workspace-split

Conversation

@yaohaowei0914
Copy link
Copy Markdown
Contributor

@yaohaowei0914 yaohaowei0914 commented Apr 20, 2026

Closes #69. Advances #70 (partial — cc-types deferred, see issue body). Part of #68.

Summary

Pure path reshuffle — converts the single-crate layout into a Cargo
workspace without changing source code semantics.

  • Root Cargo.toml becomes a virtual [workspace] with
    members = ["crates/*"] and one [workspace.dependencies] block
    holding all shared dep versions.
  • All existing sources move under crates/claude-code-rs/ via
    git mv (rename tracking preserved).
  • crates/claude-code-rs/Cargo.toml references shared deps with
    dep = { workspace = true }, keeping crate-local feature layering.
  • build.rs moved into the package and now derives the web-ui/
    path via CARGO_MANIFEST_DIR + ../.. (cargo has no
    workspace-level build script, so keeping it at root was not
    viable — deviated from the design doc here, with a note).
  • rust-embed's #[folder] attribute and the settings-schema
    snapshot test pick up the same two-level offset to the workspace
    root.
  • .cargo/config.toml, Cargo.lock, web-ui/, ui/, docs/, and
    architecture/ stay at the workspace root.

Baseline measurements

Recorded in docs/workspace-split-measurements.md:

Scenario Time
touch src/main.rs (incremental) 6.74s
touch src/tools/file_read.rs (incremental) 0.42s
Cold cargo build --release deferred to Phase 8

Cold release is skipped intentionally — the shared target-dir
(F:/cargo-target/cc-rust) is also used by other in-flight
worktrees, and cargo clean would churn caches unrelated to this
measurement. Phase 8 already requires a full rebuild, so the cold
figure lands there alongside the "after" number.

CI adaptations (commits 3c6c176, e36deb4)

Two follow-up commits after the Phase 0 move landed to get CI
actually running (rust-lite HEAD has been red on every push for
days — the Phase 0 move did not introduce that redness, it only
moved the failure from "build blocker" to "test fragility"):

3c6c176 — CI config fixes

  • .cargo/config.toml: removed target-dir = "F:/cargo-target/cc-rust".
    That is a developer-local absolute path; on GitHub Actions it caused
    failed to create directory F:/cargo-target before cargo even
    started building. Dev-side override now documented in the same file
    pointing to ~/.cargo/config.toml or CARGO_TARGET_DIR.
  • Dockerfile.ci and ui/Dockerfile.test: replaced COPY src/ /
    COPY tests/ with COPY crates/ to match the new workspace layout.
    The "empty-shell cache warmup" trick (only works for single-crate
    layouts) was dropped; we rely on BuildKit layer caching instead.

e36deb4 — Long-standing Linux-only bugs

These three errors reproduce on rust-lite@ffd38b7 (run
24679262954
timestamp 2026-04-20T16:59). All predate Phase 0. They were hidden
because the CI config problems above short-circuited the Docker build
before cargo build even started.

  • sandbox/availability.rs:89which::which used in the Linux code
    path but which was declared only in [dev-dependencies]. Promoted
    to a regular dependency.
  • computer_use/screenshot/linux.rs:13,18or_else on an async
    future can't short-circuit across awaits (E0277 / E0308). Replaced
    with a flat match .await dispatch.
  • computer_use/input/linux.rs:132 — E0515. p.to_lowercase().as_str()
    returned an &str tied to a temporary String. Rebound to a named
    local; arms now return owned String.

Phase 1 progress (commits 9fd7229, ef45df3)

Two of the three leaf crates in the #70 checklist are extracted here.
Build + test pass at every commit boundary.

cc-keybindings (9fd7229) — 2,362 LOC, 7 files

  • Pure mechanical move to crates/cc-keybindings/. Direct deps: serde,
    serde_json, parking_lot, crossterm, tracing (+ tempfile dev).
  • Root crate keeps crate::keybindings::… path unchanged via
    pub use cc_keybindings as keybindings; at crate root.
  • cargo test -p cc-keybindings: 51/51 pass.
  • cargo test -p claude-code-rs --bins --test-threads=1: 1804/1805
    (single pre-existing off_mode_skips_redaction flake, unrelated).

cc-observability (ef45df3) — 990 LOC, 4 files

  • Design doc labelled observability as a "pure leaf" but grep shows one
    tie: sink.rs:122crate::config::paths::runs_dir. Broken by
    dependency injection: AuditSink::init now takes a runs_dir: PathBuf
    parameter; main.rs:605 resolves the path at the call site before
    constructing the sink.
  • Root crate keeps crate::observability::… path via
    pub use cc_observability as observability; alias.
  • cargo test -p claude-code-rs --bins --test-threads=1: 1794/1794
    (parallel runs hit pre-existing HOME/CC_RUST_HOME env races in 9
    unrelated tests; single-threaded is clean).
  • cargo test -p cc-observability: 10/11 (same pre-existing
    off_mode_skips_redaction flake, now resident in the extracted crate).

cc-typesdeferred

Grep shows types::app_state::AppState references teams::types,
keybindings (now cc_keybindings), ui::status_line,
config::settings, and types::tool::ToolUseContext references
ipc::agent_channel. Not a true leaf. Per the routine's analysis in
issue #70, cc-types extraction is deferred until enough of
teams/ui/config/ipc have migrated (likely during Phase 5+ when the hub
cycles break). Design doc's "leaf" claim for cc-types should be
revisited as a small follow-up doc amendment.

Verification

  • cargo build (dev) : ok, 5 warnings (identical to pre-split).
  • cargo build --release : ok, 5 warnings (identical to pre-split).
  • cargo test --bin claude-code-rs --offline -- --test-threads=1
    : 1856 / 1857 pass. The single failure is
    observability::sink::tests::off_mode_skips_redaction — a
    pre-existing bug in redact_value (always redacts regardless of
    RedactionMode::Off). File last modified in 856dc41, so this is
    not something the workspace move introduced.
  • Without --test-threads=1, up to ten additional tests flake on
    env/cwd races — also pre-existing.
  • claude-code-rs --version and --help produce identical output
    to rust-lite.

Known failing CI tests (not blockers for this PR)

rust-lite CI has never successfully run any test — it was red on
compile since at least ffd38b7. Peeling back the build blockers in
this PR exposed the underlying test state for the first time.

test-windows — 1794 / 1802 passing (99.6%)

Test Likely cause Category
observability::sink::tests::off_mode_skips_redaction redact_value bug, pre-existing (pre-Phase-0) pre-existing
plugins::tests::test_paths Asserts path contains .cc-rust; CI runner HOME lacks the dir CI-env fragile
session::storage::tests::test_stable_workspace_path_uses_git_root Relies on a particular git config CI-env fragile
utils::git::tests::test_find_git_root Same CI-env fragile
teams::helpers::tests::test_add_member Temp-dir read fails on windows-latest CI-env fragile
daemon::memory_log::tests::test_today_log_path_format Timezone / path format CI-env fragile
tools::hooks::execution::tests::test_execute_command_hook_plain_text Shell not bash on runner CI-env fragile
tools::hooks::execution::tests::test_execute_command_hook_echo Same CI-env fragile

Locally with the same codebase the suite runs 1856/1857 (the routine
reported 1804/1805 earlier on a slightly older SHA). The extra 7 CI
failures are all tests that pass locally — classic "CI environment
≠ developer machine" fragility, not regressions from this PR.

test-rust-offline — 4 / 8 e2e suites green, the rest have 1–3 assertion failures each

Failures observed include:

  • print_mode_bash_tool_no_crash_without_api
  • print_mode_read_tool_no_crash_without_api
  • system_prompt_omits_send_message_by_default

These assert on CLI stdout with no API key configured. Need inspection
to determine whether the Phase 0 path move shifted any path-dependent
string they compare against. Not blocking, but worth triage as
follow-up work during Phase 1+ crate extractions (file-path references
inside tests will naturally get revisited as tests move with their
crates).

test-ui — pre-existing, not introduced by this PR

error: Cannot find package 'wrap-ansi' from '/app/ui/ink-terminal/src/core/wrapAnsi.ts'
error: Cannot find module '@alcalzone/ansi-tokenize' from '/app/ui/ink-terminal/src/core/screen.ts'

Reproduces on rust-lite@ffd38b7
(run 24679262954).
The ui/ink-terminal/ submodule's package.json is missing two
transitive deps that bun install can't resolve. Unrelated to the
workspace split — should be tracked separately.

Test plan

  • CI: cargo build --release on the workspace layout.
  • CI: cargo test -- --test-threads=1 compiles + runs (1794/1802).
  • Local smoke: cargo run -- --version, --help.
  • Tool edit re-detects: touch crates/claude-code-rs/src/tools/file_read.rs triggers rebuild.
  • cc-keybindings extracted (commit 9fd7229) — workspace crate builds + tests green.
  • cc-observability extracted (commit ef45df3) — AuditSink::init takes injected runs_dir.
  • cc-types extraction — deferred until teams/ui/config/ipc move (Phase 5+).
  • Follow-up: triage 8 CI-env-fragile Windows tests (separate PR).
  • Follow-up: triage 3 print-mode e2e assertions (likely during Phase 1 test migration).
  • Follow-up: fix ui/ink-terminal missing Bun deps (separate concern).
  • Follow-up: amend design doc to reflect cc-types non-leaf reality.

🤖 Generated with Claude Code

Crsei and others added 5 commits April 20, 2026 13:19
Part of Crsei#68. Closes (partial) Crsei#69.

Converts the repository root from a single-crate layout to a Cargo
workspace. No source code semantics change.

Layout
------
- Root `Cargo.toml` is now a virtual `[workspace]` manifest with
  `members = ["crates/*"]` and a single `[workspace.dependencies]`
  section holding all shared dep versions.
- The former single-crate sources move to `crates/claude-code-rs/`
  (preserving git history via `git mv`).
- `crates/claude-code-rs/Cargo.toml` references every shared dep via
  `dep = { workspace = true }`, keeping crate-local feature layering
  (optional flags, extra features) where needed.
- `build.rs` moves into the package (cargo has no notion of a
  workspace-level build script) and resolves `web-ui/` via
  `CARGO_MANIFEST_DIR` + `../..` so it keeps working from the new
  nested location.
- `rust-embed`'s `#[folder]` and the settings-schema snapshot test
  pick up the new two-level offset to the workspace root.
- `.cargo/config.toml`, `Cargo.lock`, `web-ui/`, `ui/`, `docs/`, and
  `architecture/` stay at the workspace root.

Baseline
--------
`docs/workspace-split-measurements.md` records pre-split incremental
build times (steady-state, same target-dir, rust-lld linker):

- `touch src/main.rs`           →  6.74s
- `touch src/tools/file_read.rs`→  0.42s

Per-phase rows will be appended as the split progresses.

Verification
------------
- `cargo build` (dev)       : ok, 5 warnings (same as pre-split).
- `cargo build --release`   : ok, 5 warnings (same as pre-split).
- `cargo test --bin claude-code-rs --offline -- --test-threads=1`
  : 1856 / 1857 pass. One failure — `observability::sink::tests::
  off_mode_skips_redaction` — is a pre-existing bug in `redact_value`
  (always redacts regardless of `RedactionMode::Off`); unrelated to
  this reshuffle, file was last touched in 856dc41.
  Without `--test-threads=1` up to ten additional tests flake on env
  /cwd races — also pre-existing.
- `claude-code-rs --version` and `--help` produce identical output.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move `src/keybindings/` (2,362 LOC, 7 files) out of `claude-code-rs`
into its own `cc-keybindings` workspace member. Zero internal deps — the
module only used std, parking_lot, crossterm, serde, serde_json, tracing,
so the extraction is purely mechanical.

Root crate keeps the `crate::keybindings::…` path working via
`use cc_keybindings as keybindings;` at crate root, so no call-site edits
were needed in main.rs, ui/app.rs, types/app_state.rs, or
commands/keybindings_cmd.rs.

All 51 keybinding unit tests pass in the new crate. Full workspace build
clean.

Part of Crsei#68 / Closes first acceptance item of Crsei#70.
Three failures on PR Crsei#79 / branch workspace-split:

1. test-windows: `failed to create directory F:/cargo-target`
   The repo-level .cargo/config.toml had `target-dir = "F:/cargo-target/cc-rust"`,
   a developer-local absolute path. Removed; reminder comment points devs to
   ~/.cargo/config.toml or CARGO_TARGET_DIR for per-machine overrides.

2. test-rust-offline: `COPY src/` in Dockerfile.ci fails — Phase 0 moved
   src/ and tests/ into crates/claude-code-rs/. Replace the old
   "empty-shell cache layer" trick (which only worked for a single crate)
   with a straight workspace copy: Cargo.toml + Cargo.lock + crates/.

3. test-ui: same src/ → crates/ move applied to ui/Dockerfile.test.

Target-dir at workspace root stays as the default `target/`, so the
binary path `/build/target/release/claude-code-rs` used by the downstream
Docker stages is unchanged.
These errors have been red on rust-lite CI for some time, previously
hidden behind the target-dir / Dockerfile-path issues that my prior
commit fixed. None are regressions from Phase 0 of the workspace split.

1. sandbox/availability.rs:89 — `which::which` used in non-test code
   but `which` was declared only in [dev-dependencies]. Promote to
   regular [dependencies]. Affects all Linux builds.

2. computer_use/screenshot/linux.rs:13,18 — `or_else` on an async
   future can't short-circuit across awaits; the chain produced
   `Result<(), ..>` vs `Result<Future<..>, ..>` in the two arms,
   tripping E0277 ("() is not a future") and E0308 (mismatched
   types). Replace with a straightforward `match .. .await`.

3. computer_use/input/linux.rs:132 — E0515. `p.to_lowercase().as_str()`
   produced an `&str` borrowing a temporary `String` that drops at
   the end of the match arm. Rebind to a local and return owned
   `String`s; unknown keys pass through preserving original case.

Only test-windows in CI exercises host code on these paths;
test-rust-offline (Linux Docker) was the one catching these.
Move observability/ (context/event/sink, 990 LOC) into a new
`cc-observability` workspace crate. Breaks the one remaining tie
to the root crate by injecting `runs_dir` into `AuditSink::init`
(previously called `crate::config::paths::runs_dir` directly).

main.rs keeps the `use cc_observability as observability;` alias so
all existing `crate::observability::...` paths resolve unchanged.

Acceptance (Phase 1, Crsei#70):
- crates/cc-observability/ created
- src/observability/ removed from root crate
- cargo build + cargo test pass (1794 bin tests, 10 cc-observability
  tests; 1 pre-existing flake `off_mode_skips_redaction` unchanged)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yaohaowei0914 yaohaowei0914 changed the title [workspace-split] Phase 0: Workspace skeleton (no code moves) [workspace-split] Phase 0 + 1 (partial): Workspace skeleton, leaf extractions, CI adaptations Apr 21, 2026
@Crsei Crsei merged commit ab8a2fc into Crsei:rust-lite Apr 21, 2026
1 of 4 checks passed
Crsei added a commit that referenced this pull request Apr 21, 2026
Move the three pure-leaf files out of `src/types/` into a new
`cc-types` workspace crate:

- `message.rs`     → `cc-types/src/message.rs`
- `state.rs`       → `cc-types/src/state.rs`
- `transitions.rs` → `cc-types/src/transitions.rs`

`app_state.rs`, `tool.rs`, and `config.rs` stay in the root crate.
Per the routine's analysis in issue #70 and the `cc-types` note in
PR #79, those three modules are not true leaves:

- `types::app_state::AppState` references `crate::teams::types`,
  `crate::ui::status_line`, `crate::config::settings`, and the
  now-extracted `cc_keybindings::KeybindingRegistry`.
- `types::tool::ToolUseContext` references
  `crate::ipc::agent_channel::AgentSender`.
- `types::config` pulls `ToolUseContext` and `Tools` from `tool`.

Extracting these would require either moving teams/ui/config/ipc
out first, or rewriting the struct layouts with trait objects /
generics — much larger than a leaf move. They stay put until those
subsystems migrate (Phase 5+).

The root crate's `types/mod.rs` now re-exports the three moved
modules via `pub use cc_types::{message, state, transitions};` so
every existing `crate::types::message::*` / `crate::types::state::*`
/ `crate::types::transitions::*` call site (~80 files) keeps
resolving without edits.

Verification
------------

- `cargo build`            : ok, 2 warnings (pre-existing
  `web::handlers::session_id` dead_code, identical on `rust-lite@ab8a2fc`).
- `cargo build --release`  : ok, same 2 warnings.
- `cargo test -p cc-types` : 0 tests (no tests moved with the files);
  compiles clean, no warnings.
- `cargo test --bin claude-code-rs --offline -- --test-threads=1`
  : **1794 / 1794 pass** (matches the `cc-keybindings` /
  `cc-observability` baselines from PR #79).
- Smoke: `claude-code-rs --version` prints `claude-code-rs 0.1.0`.

Refs issue #70.
yaohaowei0914 pushed a commit to yaohaowei0914/claude-code-rust that referenced this pull request Apr 21, 2026
… crates

Three Phase-2 extractions in one commit; each builds + tests green at
the boundary, and the root crate keeps every `crate::{auth,bootstrap,
skills}::…` path working via `use cc_X as X;` aliases at the crate root
(same pattern Crsei#79 used for `cc-keybindings` and `cc-observability`).

cc-bootstrap — 7 files, 922 LOC (clean leaf)
-------------------------------------------

`src/bootstrap/{diagnostics,ids,mod,model,signal,state,timing}.rs` →
`crates/cc-bootstrap/src/…` with a pure `git mv`. No dependency-injection
needed: `bootstrap/` was already a true import-DAG leaf per its own
module doc. Direct deps: `serde`, `parking_lot`, `tokio`, `uuid` (+
`serde_json` for the round-trip test in `ids.rs`).

cc-auth — 8 files, ~1,500 LOC (one tie broken)
---------------------------------------------

`src/auth/{api_key,codex_cli,mod,token}.rs` and
`src/auth/oauth/{mod,client,config,pkce}.rs` → `crates/cc-auth/src/…`.

The one reverse-dep was `token.rs:9` calling
`crate::config::paths::credentials_path()`. Broken by a global init
registered from the host at process startup (mirrors the `set_event_sender`
pattern already used by mcp/plugins/lsp/skills):

- `cc_auth::set_credentials_path(PathBuf)` — host calls this at the top
  of `fn main()` with `crate::config::paths::credentials_path()`.
- `token::token_file_path()` reads it back via a private
  `crate::credentials_path()` helper.
- Falls back to `{CC_RUST_HOME | ~/.cc-rust | $TMP/cc-rust}/credentials.json`
  when the host hasn't registered one, so unit tests that exercise
  `resolve_auth()` directly (doctor, logout, voice_cmd — 10 tests) keep
  passing. The fallback duplicates ~10 LOC from `config::paths::data_root`
  — a small price for full decoupling.

Keychain service name stays `"cc-rust"` (the original acceptance
criterion on Crsei#71). OAuth scopes, PKCE, and token-refresh flow are
untouched — only the storage path resolution changed.

Direct deps: `anyhow`, `serde`, `serde_json`, `chrono`, `tokio`,
`tracing`, `reqwest`, `keyring`, `base64`, `rand`, `sha2`, `dirs`,
`parking_lot`, `urlencoding`.

cc-skills — 3 files, ~1,000 LOC (two ties broken)
------------------------------------------------

`src/skills/{bundled,loader,mod}.rs` → `crates/cc-skills/src/…`.

Two reverse-deps broken:

1. **Event emission** — `emit_event` held a
   `broadcast::Sender<crate::ipc::subsystem_events::SubsystemEvent>`,
   which is a cycle the moment skills leaves the root crate.
   Replaced with a minimal cc-skills-owned enum and a callback:

   ```rust
   pub enum SkillSubsystemEvent { SkillsLoaded { count: usize } }
   pub fn set_event_callback<F: Fn(SkillSubsystemEvent) + Send + Sync + 'static>(cb: F);
   ```

   The host adapts it into `SubsystemEvent::Skill(SkillEvent::SkillsLoaded { .. })`
   in `ipc/runtime.rs` (replaces the old `set_event_sender(bus.sender())`
   line).

2. **User-skills directory** — `init_skills` used to resolve
   `crate::config::paths::skills_dir_global()` internally. It now takes
   the directory as its first parameter; `main.rs` and `ipc::subsystem_handlers`
   pass `&crate::config::paths::skills_dir_global()` at the call site.

Direct deps: `serde`, `parking_lot` only.

Verification
------------

- `cargo build`                  : ok, 2 warnings (pre-existing
  `web::handlers::session_id` dead_code, identical on `rust-lite@ab8a2fc`).
- `cargo build --release`        : ok, same 2 warnings.
- `cargo test --workspace --lib --offline --no-fail-fast -- --test-threads=1`:
  - `cc-auth`          — 32 passed, 0 failed, 2 ignored
  - `cc-bootstrap`     — 28 passed, 0 failed
  - `cc-keybindings`   — 51 passed, 0 failed (unchanged from P1)
  - `cc-observability` — 10 passed, 1 pre-existing flake
    (`off_mode_skips_redaction`, tracked in Crsei#79)
  - `cc-skills`        — 21 passed, 0 failed
  - `cc-types`         — 0 tests
- `cargo test --bin claude-code-rs --offline -- --test-threads=1`
  : **1713 / 1713 pass**. Baseline from PR Crsei#80 was 1794 — the 81-test
  drop is the tests that moved out with their code (cc-bootstrap 28 +
  cc-auth 32 + cc-skills 21 = 81, matches exactly).
- Smoke: `claude-code-rs --version` prints `claude-code-rs 0.1.0`.

Phase 2 checklist (Crsei#71)
-----------------------

- [x] `crates/cc-bootstrap/` created
- [x] `crates/cc-auth/` created
- [x] `crates/cc-skills/` created
- [x] `src/{bootstrap,auth,skills}/` removed from root crate
- [x] Keychain service name unchanged (`"cc-rust"`)
- [x] All tests pass (1713 bin + 142 extracted, one pre-existing
  cc-observability flake unchanged)
- [ ] Manual OAuth login E2E (`/login 2` + `/login-code`) — not
  reproducible in CI, flagged for maintainer verification.

Refs Crsei#71. Part of Crsei#68.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[workspace-split] Phase 0: Workspace skeleton (no code moves)

2 participants