Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
WORKSPACE.md (25K)
1 # The cce workspace
2
3 Guidance for working anywhere in the `cce` Wayland desktop workspace — the layout,
4 the multi-repo rule, the build/install entry point, config, and IPC.
5
6 **This file lives here, not at the workspace root, because the root is not a git
7 repository** (see the multi-repo section below) — anything written there is
8 unversioned and lost on a fresh clone. The root `CLAUDE.md` is a pointer to this
9 file and repeats only the two rules that must not be acted against before reading
10 it. Per-crate notes stay in each crate's own `CLAUDE.md`; compositor-specific
11 detail is in the adjacent `CLAUDE.md`.
12
13 **Paths below are relative to the workspace root** — the parent of this crate — so
14 `cce-ui/src/` means `../cce-ui/src/` when read from here.
15
16 ## What this is
17
18 This is the **`cce` Cargo workspace** (`resolver = "2"`). It is a complete Wayland
19 desktop environment written in Rust, split into two halves:
20
21 - **`cce-compositor/`** — the compositor + tiling window manager (`cce-fx`, symlinked
22 as `cce`), built on wlroots 0.20 via FFI with vendored scenefx. This crate is its own
23 world: it has a `build.rs` native-build pipeline, a `Makefile`, and its own detailed
24 **`cce-compositor/CLAUDE.md`** — read that before working inside `cce-compositor/`.
25 - **~18 `cce-*` client apps** (`cce-status-interface`, `cce-system-interface`,
26 `cce-designer`, `cce-files`, `cce-color-editor`, `cce-mail`, `cce-graph`, `cce-notifier`,
27 `cce-authenticator`, `cce-display-manager`, `cce-text-editor`,
28 `cce-data-editor`, `cce-fonts`, `cce-cloud`, `cce-layout-interface`,
29 `cce-screenaver`, `cce-gallery`, `cce-terminal`, …) — Wayland client GUIs that connect to the
30 compositor and to each other over Unix sockets. (The desktop background is drawn
31 natively by the compositor — the former `cce-wallpaper` client was retired.)
32
33 The one thing tying every crate together is **`cce-ui`**, the shared GUI toolkit. Every
34 client depends on it (`cce-ui = { path = "../cce-ui" }`); the compositor depends on it
35 too. There is one other shared crate: **`cce-window-manager`** — the compositor's
36 pure-Rust window-management policy layer (arrange pass, `TilingMode`, saved state,
37 slotmap; no FFI), extracted from `cce-compositor/` and consumed only by it. The compositor
38 re-exports it as `crate::policy` / `crate::tiling` / `crate::slotmap`.
39
40 ### Version control: this is a MULTI-repo, not a monorepo
41
42 The workspace root itself (this directory — holding `Cargo.toml`, `Cargo.lock`,
43 `target/`) is **not** under version control. Instead, **each member crate is its own
44 independent git repository** with its own committed `Cargo.lock`. The crates sit
45 side-by-side under this directory to form the build workspace, but are versioned and
46 published separately.
47
48 **Committing is not publishing — pushing is.** Every crate's `origin` is
49 **GitHub** (`https://github.com/lsgalante/<crate>.git`), and it is the only
50 remote: the local bare repos under `~/git/` and the `published` remote are gone
51 (since 2026-09-20). A `post-commit` hook, symlinked into each repo by gitsite's
52 `install-hooks.sh`, pushes the branch just committed, so in the normal case a
53 commit is on GitHub seconds later — but a crate without the hook, or a push
54 that failed, leaves the commit on no remote at all, and nothing warns.
55 `git log origin/<branch>..` is the check.
56
57 **git.lucas.co is a read-only mirror of GitHub**, not a publishing step.
58 gitsite (`~/projects/gitsite`, run hourly by the `gitsite.timer` user unit via
59 `autodeploy.sh`) `ls-remote`s every repo in its `repos.conf` — whose path
60 field is now the GitHub URL — and when any HEAD moved, re-mirrors, renders and
61 deploys the static site (browsable, clonable over dumb HTTP for `clone`-mode
62 entries). So the chain is `git commit` → hook → GitHub → `gitsite.timer` →
63 the site, with up to an hour of lag at the last step. New crates get a line in
64 `repos.conf`. (The pre-2026-08-11 per-crate codeberg.org remotes are retired;
65 those repos still exist server-side for old history.)
66
67 The **Cargo git pins name GitHub too** (since 2026-09-21): every app declares
68 `cce-ui` (and the compositor `cce-window-manager`) as
69 `{ git = "https://github.com/lsgalante/<dep>.git", rev = "<sha>" }`, and the
70 root `[patch]` block keys on that same URL to redirect it to the local crate.
71 They pointed at git.lucas.co before, which meant a rev pinned right after a
72 push was unfetchable for up to an hour of mirror lag. Keep the two URLs
73 identical: a patch whose key does not match the pin is silently unused, and
74 every crate then builds a fetched copy of the toolkit instead of the tree.
75 `bump-revs.sh` at the root repins after a shared crate is pushed; it reads the
76 rev from GitHub itself, and refuses while the dependency's work tree is dirty
77 or ahead of origin.
78
79 Consequences to respect:
80 - **Do not `git init` at the root** — it would swallow every crate as an embedded repo.
81 Commit inside the relevant crate's own repo.
82 - **Each crate must build standalone.** Do not introduce `[workspace.dependencies]` /
83 `<dep>.workspace = true`: a standalone clone of a single crate's repo has no
84 `[workspace]` parent, so inherited deps fail to resolve. Dependency versions are
85 intentionally declared per-crate (minor drift between independent crates is fine).
86 - A crate's `[profile.*]` is honored when it's built standalone (it is then its own
87 workspace root) and ignored (with a warning) in the full-tree build — that warning is
88 expected, not a bug to "fix" by deleting the profile.
89
90 ## Build, test, run
91
92 The workspace `target/` dir is shared at the repo root (`./target/`). There is no root
93 Makefile, but **`ccebuild` is the DE-wide entry point** — do not hand-roll a loop over
94 the crates. It ships in `cce-compositor/scripts/ccebuild` and installs to
95 `~/.local/bin`:
96
97 ```sh
98 ccebuild install # build the workspace, install every binary + unit
99 ccebuild install cce-mail # just one package (what each crate's `make install` runs)
100 ccebuild restart # restart user services left on a replaced binary
101 ccebuild status # built-vs-installed drift, AND running-vs-installed
102 ccebuild prune # target/ artifacts of crates cargo no longer knows
103 ccebuild install-system # the root-owned binaries, units, PAM stacks, udev rules (one sudo prompt; --dry-run to preview)
104 ```
105
106 The full deploy loop is `ccebuild install && ccebuild restart`. `ccebuild` derives
107 every binary from `cargo metadata`, which is the point: the per-crate Makefiles used
108 to name their binaries by hand, so crates with extra `[[bin]]` targets shipped
109 incomplete for weeks (`cce-ui` without `cce-relief`, `cce-display-manager` without its
110 three `cce-keyring-unlock*` helpers). Each crate's `make install` is now a thin
111 wrapper around `ccebuild install --no-build <pkg>`; `make build/run/clean` are
112 unchanged. **Never add a binary name to a Makefile** — cargo already knows it.
113
114 ### Desktop entries
115
116 A crate that should appear in the launcher — or be selectable as an XDG default —
117 ships **`<crate>/<name>.desktop` at its own root**, next to `Cargo.toml` and beside
118 any `*.service` it ships. `ccebuild install` copies those into
119 `$XDG_DATA_HOME/applications` and runs `update-desktop-database`, filtered by
120 package the same way units are.
121
122 Two rules, both learned the hard way when these files lived only in
123 `~/.local/share/applications` and were hand-edited there:
124
125 - **`Exec=` is a bare binary name**, never an absolute path. `~/.local/bin` is the
126 first entry on the session PATH, and the launcher (`cce-cloud`) spawns through
127 `sh -c`, so the name resolves. Nine of the ten imported entries had baked in
128 `/home/lsgalante/.local/bin/…`.
129 - **An app is only reachable as a default handler if it declares `MimeType=`.** The
130 settings app's Default Apps page builds each dropdown by scanning installed
131 entries for the ones claiming that category's MIME types, so an app with no
132 `MimeType=` line simply never appears as a candidate — which is why `cce-files`
133 could not be chosen as the file manager despite having an entry. Declaring a type
134 also means honoring it: the app has to accept the path or URL argv the field code
135 (`%f`/`%u`) passes it.
136
137 ### App icons
138
139 An entry's `Icon=` should be the app's own name (`Icon=cce-files`), backed by
140 `cce-icons/hicolor/scalable/apps/cce-files.svg`. `ccebuild install` mirrors any
141 crate's `hicolor/` tree into `$XDG_DATA_HOME/icons/hicolor/` and refreshes the GTK
142 icon cache; `cce-icons/hicolor/README.md` documents the naming and the symlink
143 convention that keeps `svg/` the sole source of the artwork.
144
145 Before 2026-08-16 the entries borrowed generic freedesktop names
146 (`preferences-system`, `system-file-manager`), which resolved only if some other
147 installed theme happened to provide them, and `cce-preview` "worked" only because
148 five PNGs had been hand-copied into `~/.local/share/icons` — unversioned, and gone
149 on a fresh clone. The same failure as the `.desktop` files themselves.
150
151 Note that **nothing displayed an `Icon=` key at all** until the launcher was taught
152 to: `cce-cloud`'s `AppInfo` had no icon field. `cce_ui::icon` is the shared
153 resolver (theme name or absolute path → file); it is distinct from
154 `cce_ui::upload_icon`, which loads a *bundled* cce-icons glyph for in-widget use.
155
156 `ccebuild status` is the tool for "is what's running actually the code I built?".
157 Because `install` unlinks before writing, a process still on the old inode reports its
158 exe as `(deleted)`, which is how both `status` and `restart` detect drift. It also
159 catches apps launched straight out of `target/` rather than `~/.local/bin`.
160
161 **But it cannot see a client that is stale against `cce-ui`.** The toolkit is a
162 static Rust library, so committing and installing `cce-ui` itself changes nothing
163 about the ~20 crates that link it — each has to be rebuilt and reinstalled before
164 it carries the change. `status` compares each binary's mtime in `target/release`
165 against the one in `~/.local/bin`, so a client nobody rebuilt has both old and
166 equal and reads as up to date. It is stale against a *dependency*, the one kind of
167 staleness that check has no notion of.
168
169 Then a **running** process keeps its old inode until it is relaunched, and
170 `cce-fx` keeps its own until the next login. So a toolkit fix lands in three
171 stages — commit, rebuild dependents, relaunch — and it is the middle one that
172 gets skipped. Learned from cce-ui@2416904, which raised each client's
173 `RLIMIT_NOFILE`: the fix was committed and cce-ui installed, and every client
174 still ran at the old limit until its own crate was rebuilt, thirteen of them.
175
176 Sweep with one cargo invocation over the dependents (`cargo build --release -p …
177 -p …` — one shape, since alternating with a bare `--workspace` build re-resolves
178 features and invalidates crates, as below), then `ccebuild install
179 --no-build <crate>` for each. Verify by looking *inside* the installed binary for
180 something the change introduced — `strings ~/.local/bin/<crate> | grep -q
181 '<new log string>'` — rather than trusting that the build ran. (`cce-browser` belongs in the sweep
182 too, but for a different reason than previously recorded here: since
183 2026-08-30 its **default build is WPE WebKit** against the system
184 `libWPEWebKit` — seconds, ~14 MB, no Servo compiled at all. The old
185 leave-it-out rule dated from Servo being the default engine — always the
186 crates.io package, never vendored — which cost more than the rest of the
187 workspace combined; that backend still exists behind `--no-default-features
188 --features servo` and is still that expensive, so only build it deliberately.
189 The default flip is itself a lesson for sweeps: while WPE was opt-in, a
190 featureless sweep rebuild silently reverted the installed browser to the
191 wrong engine. Defaults are what sweeps build; an opt-in variant of a binary
192 does not survive one.)
193
194 **A hit proves freshness; a miss proves nothing.** Not every string literal in
195 the source survives into the binary, and the two cases are not distinguishable
196 from the outside. Measured against a current `cce-fx` on 2026-08-28: the live
197 `Action` names `mode_next_shared` and `toggle_overview` appear (twice and once),
198 while `overlay_right`, `brightness_down` and `focus_up` — same file, same kind
199 of literal, all reachable in `cce-window-manager/src/api.rs` — report zero.
200 Probably link-time constant merging; recorded as observed, not explained. So
201 prefer a long distinctive log string over a short match-arm literal, and never
202 read a zero as "the build didn't take" — that false negative has already cost a
203 session an afternoon of chasing a build that had worked.
204
205 When the answer actually matters, test the behavior instead of a proxy for it:
206 put a deliberately bogus value where the real one goes (a made-up action name in
207 a shadow session's `input.kdl`) and watch for the code path that rejects it —
208 the compositor's "unknown window-manager action" warning firing for the bogus
209 name and staying quiet for yours proves the running binary knows yours.
210
211 For plain cargo work:
212
213 ```sh
214 cargo build --release # build every crate
215 cargo build -p cce-status-interface # build one client
216 cargo run -p cce-system-interface # run one client
217 cargo test --workspace # all tests (tests are sparse)
218 cargo test -p cce-fx --lib config:: # tests in one module of one crate
219 ```
220
221 Building the workspace compiles the **compositor** too, which triggers its `build.rs`
222 (meson/ninja to build vendored scenefx, wayland-scanner for protocols, bindgen over
223 wlroots). That needs native system deps — see `cce-compositor/CLAUDE.md` for the full list. If you
224 only touch a client, prefer `-p <crate>` to avoid rebuilding the compositor — but note
225 that alternating `cargo build --release` with `cargo build --release -p <crate>`
226 resolves different unified feature sets, so each invocation re-invalidates a few
227 crates (~11s). Pick one shape and stay with it.
228
229 Binary names do not reliably match the crate: `cce-fx` lives in `cce-compositor/`,
230 `cce-system-interface` and `cce-files` declare explicit `[[bin]]` names, and several
231 crates ship extra bins (`cce-ui` → `cce-ramp`/`cce-relief`, `cce-compositor` →
232 `ccectl`). Ask cargo rather than guessing:
233 `cargo metadata --no-deps --format-version 1 | jq -r '.packages[].targets[] | select(.kind|index("bin")) | .name'`.
234
235 ## The `cce-ui` toolkit (start here for any client work)
236
237 `cce-ui` is a **custom retained-mode GUI toolkit**, not a wrapper around an existing
238 framework. Understanding it is the prerequisite for touching any client.
239
240 - **Transport**: raw `wayland-client` 0.31 + `smithay-client-toolkit` 0.19, driven by a
241 `calloop` event loop. Clients are real Wayland surfaces, not toolkit windows.
242 - **Rendering**: raw Vulkan via **ash** (`cce-ui/src/vk/` — `VkRenderer`; the wgpu
243 path was retired), with **cosmic-text** for text shaping (depended on directly
244 since the wgpu retirement — it used to be reached through glyphon, whose only
245 other export was the wgpu renderer nothing here used). Widgets emit
246 vertex batches (quads, rounded rects, vectors, arcs, circles) — see the re-export
247 list in `cce-ui/src/engine.rs`. There is no HTML/DOM; the UI is drawn as GPU
248 primitives.
249 - **The `Application` trait** (`cce-ui/src/backend/window_runner.rs`) is the contract
250 every client implements. Key methods: `new`, `settings`, `update(msg)`, `tick(dt)`,
251 `display_list` (the single paint path) plus `overlay_quads` / `custom_vertices`,
252 and the input hooks (`handle_pointer_move`, `handle_mouse_input`, …). Apps needing
253 direct renderer access (3D scenes, app-shaped text, non-rect window chrome) use the
254 extended hooks `renderer_init` / `stage_renderer` / `standard_csd` /
255 `take_window_action` — `cce-designer` is the reference consumer. A client's
256 `main.rs` is typically a struct implementing `Application` plus a one-line
257 `cce_ui::engine::run::<MyApp>();`.
258 - **Modules**: `widget/` (containers, inputs, editor, `json_layout`), `layout.rs`
259 (fonts + sizing, lots of `*_font_parsed()` getters), `color.rs`, `config.rs`,
260 `protocol.rs` (talking to the compositor), `context.rs`, `process.rs`,
261 `file_dialog.rs`, `scale.rs` (HiDPI), `units.rs` (lengths with units — `(mm)` config
262 values — and the display metric from EDID), `mcp.rs` (tools-only MCP server over
263 Streamable HTTP so apps can expose their state/actions to AI agents —
264 `cce-designer` is the reference consumer, see its CLAUDE.md).
265
266 When adding a widget or a client, mirror an existing client (e.g.
267 `cce-status-interface`) rather than inventing a new structure.
268
269 ## Configuration (shared across the whole DE)
270
271 Config is **KDL** (`kdl` crate), loaded from `~/.config/cce/` (honoring
272 `XDG_CONFIG_HOME`), via `cce-ui/src/config.rs`:
273
274 - **`~/.config/cce/config.kdl`** — the shared/global config (`get_config_path()`).
275 - **`~/.config/cce/<app-name>/config.kdl`** — per-app override
276 (`get_app_config_path(app_name)`).
277 - **`~/.config/cce/input.kdl`** — DE-wide keybindings and pointer input settings,
278 domain-scoped (`cce-ui/src/input.rs`): top-level nodes are domains
279 (`cce-window-manager` for compositor actions, `cce-ui` for toolkit-wide widget
280 defaults, `cce-<app>` for per-app bindings), children are `name "chord"`
281 bindings. Resolution for an app is `<app>.<name>` → `cce-ui.<name>` (the
282 toolkit-wide `undo` / `redo` chords live here — `cce-ui/src/history.rs`); the
283 compositor maps its domain onto `cce-window-manager::api::Action` via the
284 policy crate's `bindings` module. Legacy keybind entries in `config.kdl` still
285 load; `input.kdl` wins on conflict. `ccectl migrate-input` extracts config.kdl
286 keybindings into input.kdl (with backup; config.kdl is never rewritten).
287 A top-level `input { }` block holds global pointer hardware defaults with
288 per-device-class sub-blocks (`mouse` / `trackpad` / `trackpoint`: accel,
289 scroll_factor…), consumed by the compositor; an `input { }` child inside an
290 app domain holds that app's scroll overrides, applied client-side by cce-ui
291 (pixel deltas scale as trackpad, discrete wheel clicks as mouse). Smooth
292 scrolling is tuned by the same keys on both sides — `smooth_scroll`,
293 `scroll_ease`, `kinetic_scroll`, `scroll_friction` — read by cce-ui from
294 the app/`cce-ui` domain (`cce-ui/src/widget/scroll_motion.rs`, the one
295 wheel→offset model every scrolling widget and app-owned list drives) and
296 by the compositor from the global block for its own desktop pans. Keybinding
297 and input edits are made directly on the file (e.g. via cce-data-editor) —
298 there is deliberately no dedicated settings UI.
299 - Config edits are backed up under `~/.config/cce/backups/config.kdl.<n>.bak`.
300
301 The compositor additionally runs `~/.config/cce/init` on startup and persists window
302 state to `~/.local/state/cce/state.json` — details in `cce-compositor/CLAUDE.md`.
303
304 ## How the pieces talk (IPC)
305
306 Clients and compositor communicate over Unix sockets keyed by `$WAYLAND_DISPLAY`:
307
308 - **Control**: `/tmp/cce-{WAYLAND_DISPLAY}.sock` — line-oriented request/reply. The
309 `ccectl` binary (in `cce-compositor/`) is the CLI client; run `ccectl` with no args for the
310 command list.
311 - **Status**: `/tmp/cce-status-{WAYLAND_DISPLAY}.sock` — subscribe to `layout` /
312 `title` / `modifiers` / `adjust` / `dismiss` / `backdrop <app_id>` and receive push
313 updates. `adjust` is "on"/"off" as window-adjust mode (overview, or Super
314 held) comes and goes — what `cce-grid` keys its image resize handles on.
315 This feeds `cce-status-interface` (the status bar). `backdrop` is the odd one
316 out: it takes the asking segment's app_id and reports what that segment is
317 composited over, which is the one thing a Wayland client can never see for
318 itself. (The `viewport` topic went with the viewport-tag feature.)
319
320 ## Window fades (DE-wide open/close dissolve)
321
322 Every window and overlay the user opens dissolves in when it maps and out when
323 it closes. **The fade is the compositor's, in both directions** — it ramps the
324 opacity of the client's scene subtree
325 (`river_scene_node_set_opacity`, which also carries the scenefx backdrop blur,
326 drop shadow and bevel), so a whole window crossfades against the desktop rather
327 than each of its elements crossfading against each other.
328
329 - **In** is automatic and needs nothing from the client: `Window::map` starts
330 the ramp for toplevels, `handle_layer_surface_map` for Overlay-layer
331 surfaces. Desktop furniture opts out — status segments, the wallpaper, the
332 grid layer (`Window::wants_map_fade`), and the Background/Bottom/Top layers —
333 because those map once at login, where a dissolve reads as the desktop
334 failing to draw.
335 - **Out** needs one thing from the client, because a surface that is already
336 destroyed cannot be faded: it sends `fade-out` on the control socket, is
337 answered with a duration in ms, and keeps its surface mapped and its process
338 alive for exactly that long before exiting. `cce_ui::ipc::request_close_fade()`
339 is that call, and `window_runner` already makes it for every `Application`, so
340 an ordinary cce client gets the close fade for free. An app driving its own
341 event loop calls it itself — `cce-cloud` is the worked example.
342 - The duration is `surface { fade in_ms=140 out_ms=120 }`, clamped to 2s. It is
343 answered back over the socket rather than duplicated in the client, so the
344 two halves cannot drift when the config changes. `0` disables that direction,
345 and a client that gets `0` exits immediately.
346 - The target is resolved from the caller's **pid** (SO_PEERCRED on the control
347 socket), not from a name in the command: the kernel vouches for it, and a
348 client always knows its own pid even when it has no app_id.
349
350 **Do not fade a window from inside the client.** It cannot work: the surface
351 stays fully present to the compositor however transparent the client draws
352 itself, so the blur behind it hangs at full strength over a dissolving window —
353 and in cce-ui specifically, shader-lit output (SDF plate rims, specular) is not
354 vertex-alpha and does not fade with the geometry at all. `cce-cloud` carried
355 exactly that for months; its close fade dropped the plate batches to hide the
356 un-fading rims, which deleted the window's whole background on the fade's first
357 frame, since a plate batch **is** its cover quad.
358
359 Note the usual toolkit-staleness trap (above): the fade-IN is entirely
360 compositor-side and appears the moment a new `cce-fx` is running, but the
361 fade-OUT rides `cce-ui`, so a client nobody rebuilt fades in and then vanishes.
362 That asymmetry is the symptom of a missed sweep, not of a broken fade.
363
364 ## Repo hygiene
365
366 The repo root and `cce-compositor/scratch/` are littered with **ad-hoc debugging artifacts** — many
367 `screenshot_*.png`, `*.log` (some enormous, e.g. `debug.txt`, `dropbox_strace.log`),
368 and one-off `*.py` inspection scripts (`patch*.py`, `scan_*.py`, `inspect_*.py`). These
369 are **not part of the build**. Don't treat them as source, and don't add more to the
370 root; use the scratchpad directory for temporary files.
371
372 ## Concurrent sessions (multiple agents in this workspace)
373
374 Several Claude Code sessions may be working in sibling crates **at the same
375 time**. The workspace shares one `target/`, one `~/.local/bin`, and one live
376 compositor session between them, so an unscoped command in one session damages
377 the others. The rules:
378
379 - **Scope builds and installs to your crate.** `cargo build --release -p
380 <crate>` and `ccebuild install --no-build <crate>` — never a bare
381 `ccebuild install`, which deploys *every* crate's most recent build,
382 including another session's half-finished work. A concurrent build blocking
383 on cargo's build-directory lock ("Blocking waiting for file lock") is
384 normal — wait it out; don't kill it or conclude the build is broken.
385 - **Restart with `ccebuild restart <crate>`, never the bare form.** Bare
386 `ccebuild restart` is unscoped: it restarts every user service running a
387 replaced binary, including apps another session has installed but is not
388 ready to restart. The per-crate form restarts only units shipped by the
389 named crate(s). Apps that are not services restart by pid, not name:
390 `pkill -x` matches the kernel comm name, which is truncated to 15
391 characters, so it silently matches NOTHING for most `cce-*` binary
392 names ("cce-status-interface" is 20) — a "kill then relaunch" built on
393 it relaunches beside the survivor and doubles the app. Find the pid
394 with `ps -eo pid,ppid,cmd`, confirm it is yours via
395 `/proc/<pid>/cgroup` (a unit's processes name their unit), `kill` it
396 explicitly, then relaunch detached.
397 - **Shared crates are exclusive.** Before editing `cce-ui`,
398 `cce-window-manager`, or `cce-icons`, run `git status` there. Foreign dirt
399 means another session owns that crate right now — coordinate or stop; don't
400 edit around it. Commit your own crate's work promptly so other sessions
401 always see clean repos.
402 - **Verify in your own shadow session.** `cce-shadow start --new` gives each
403 agent a private headless compositor. Driving the *live* session (`ccectl`
404 pointer injection, screenshots, app restarts) is only safe when you know
405 you are the sole session doing so — two agents share one pointer and one
406 screen, and each contaminates the other's observations.
407 - **Coordinate through the harness.** `ListAgents` shows the other local
408 Claude sessions; `SendMessage` reaches them. Before touching a shared crate
409 that shows foreign dirt, ask the session that owns it instead of guessing.