> ## Documentation Index
> Fetch the complete documentation index at: https://openidea.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# IPC between UI and Rust

> The communication contract: typed commands returning IdeResult, generated bindings, text coordinates, streaming channels, cancellable requests and the backend event bus.

The frontend and the backend talk over Tauri IPC, with three mechanisms:

| Mechanism | Direction | Use it for |
| - | - | - |
| **Command** | UI → Rust, one reply | Every request. Returns `IdeResult<T>`. |
| **Channel** (`tauri::ipc::Channel<T>`) | Rust → UI, many values for one request | Streaming results: search hits, process output, terminal bytes, progress. |
| **Event** (`IdeEvent` via `EventSink`) | Rust → UI, unsolicited | Notifications: files changed on disk, diagnostics, index state, debug session stopped. |

All three are typed end to end: Rust types derive `specta::Type`, and tauri-specta generates `src/bindings.ts`.

## Commands

A command is a thin adapter in `src-tauri/src/commands/<domain>.rs`. It resolves the project from the calling window, calls the shared implementation (usually in `ide_host::ops`), and returns `IdeResult<T>`:

```rust src-tauri/src/commands/app.rs theme={null}
#[tauri::command]
#[specta::specta]
pub async fn project_trust_set(
    state: State<'_, AppState>,
    path: PathBuf,
    trusted: bool,
    folder: bool,
) -> IdeResult<()> {
    state.host.set_project_trust(&path, trusted, folder).await?;
    // ...
}
```

Register new commands in the `collect_commands!` list of `commands::builder()` (`src-tauri/src/commands/mod.rs`). The module's conventions:

* every command returns `IdeResult<T>` (it rejects with `IdeError` in JS);
* project-scoped commands resolve the project from the calling window;
* streaming results use a `Channel<T>` argument; cancellable requests take a UI-allocated `RequestId` and are cancelled via `cancel_request`.

Before any command runs, the invoke handler in `src-tauri/src/lib.rs` checks [project trust](https://openidea.sh/en/contribute/security.md#project-trust-and-restricted-mode) and, for a remote window, routes it to the server.

### Calling a command

The frontend imports `commands` from `@/platform/ipc` (which re-exports the bindings) and unwraps the result with `call`:

```ts theme={null}
import { call, commands } from "@/platform/ipc";

await call(commands.projectTrustSet(path, trusted, folder));
```

Generated functions return `{ status: "ok", data } | { status: "error", error }`. `call` turns that into a promise that rejects with `IpcError`, which carries the error's `kind` and `data`. Helpers: `isCancelled(e)`, `isNoServer(e)`, `unavailableText(e)`.

## Errors

`IdeError` (`crates/ide-core/src/error.rs`) is the single error type that crosses IPC:

```rust theme={null}
pub type IdeResult<T> = Result<T, IdeError>;

pub struct IdeError {
    pub kind: ErrorKind,
    pub message: String,
    /// Structured detail for errors the UI acts on; `null` for plain errors.
    pub data: Option<ErrorData>,
}
```

`ErrorKind` is one of `notFound`, `alreadyExists`, `permissionDenied`, `invalidInput`, `outOfSync` (document version mismatch; the UI must resync the full text), `cancelled`, `timedOut`, `unavailable` (a required tool such as a language server, git or a debugger is missing), `io`, `protocol`, `internal`.

Crates keep rich internal errors and **convert at their public edge**; the UI switches on `kind` and shows `message`. Don't leak `anyhow` or crate-specific errors through a command.

## Generated bindings

`src/bindings.ts` is generated by tauri-specta from the registered commands plus `IdeEvent`. **Never edit it.**

* `pnpm gen:bindings` regenerates it without launching the app (`cargo test -p openidea --test export_bindings`).
* Every debug launch also re-exports it, rewriting the file only when it changed.
* CI regenerates it in the Rust job and fails if the committed file differs. After changing a command signature or an IPC type, commit `src/bindings.ts`.

Rust doc comments on commands and types become JSDoc in the bindings, so document commands in Rust. 64-bit integers are exported as `number`.

## Text coordinates

Everything that crosses IPC uses the same coordinates (`crates/ide-core/src/text.rs`):

* **Lines are 0-based.**
* **Columns and offsets are UTF-16 code units**, matching JavaScript strings, CodeMirror and the LSP default encoding.
* `Pos { line, col }`; `Range { start, end }` is half-open.

Conversion to and from byte offsets lives in `ide-text`. Convert at the edge of your crate: a tree-sitter byte offset or an LSP position in another encoding must never reach a command's result.

## Streaming with channels

High-volume or incremental results go through a `Channel<T>` argument, not the event bus. Find in Files:

```rust src-tauri/src/commands/search.rs theme={null}
/// Find in Files. Results stream through `on_event`; ends with `Done`.
/// Cancel with `cancel_request(request)`.
#[tauri::command]
#[specta::specta]
pub async fn search_start(
    window: Window,
    state: State<'_, AppState>,
    request: RequestId,
    query: SearchQuery,
    on_event: Channel<SearchEvent>,
) -> IdeResult<()> {
    let emit = std::sync::Arc::new(move |e| {
        let _ = on_event.send(e);
    });
    ide_host::ops::search::start(&state.host, window.label(), request, query, emit).await
}
```

On the UI side, create a `Channel`, set `onmessage`, and pass it as the argument:

```ts src/platform/textSearch.ts theme={null}
const channel = new Channel<SearchEvent>();
channel.onmessage = (ev) => {
  if (ev.type === "file") onFile(ev.file);
  else finish(ev.summary);
};
call(commands.searchStart(request, query, channel));
```

Make the stream self-terminating (a final `Done` value) so the UI knows when it ended.

## Cancellable requests

The UI allocates the id; the backend registers a token for it:

```ts theme={null}
import { commands, nextRequestId } from "@/platform/ipc";

const request = nextRequestId();
const summary = streamSearch(request, query, onFile);
// later, if the user cancels:
void commands.cancelRequest(request);
```

```rust crates/ide-host/src/ops/search.rs theme={null}
let cancel = host.begin_request(request);
let events = on_event.clone();
let syntax = host.syntax();
let summary = host
    .blocking(move || {
        search_project(&project, &syntax, &query, &cancel, &|file| events(SearchEvent::File { file }))
    })
    .await;
host.end_request(request);
on_event(SearchEvent::Done { summary: summary? });
```

Details that matter:

* Ids are shared by all windows of the backend, so each page starts counting at a random point.
* A cancel that arrives **before** the command began is remembered for 120 seconds, and the request then starts out cancelled (`crates/ide-host/src/requests.rs`).
* Long work checks the `CancellationToken` and returns `ErrorKind::Cancelled` (or a partial result) promptly.
* Tasks the backend starts itself (a language server's work-done progress) are reported as `IdeEvent::Task` with a `TaskId` and cancelled with `cancel_task`, not `cancel_request`.

## Events

`IdeEvent` (`crates/ide-core/src/event.rs`) is one enum of everything the backend tells the UI unasked, serialized as `{ type: "<camelCase name>", ... }`. Feature crates emit through an `EventSink`:

```rust theme={null}
pub trait EventSink: Send + Sync + 'static {
    fn emit(&self, event: IdeEvent);
}

project.sink.emit(IdeEvent::ProjectTrustChanged { root: project.root.clone().into(), trusted });
```

`src-tauri/src/events.rs` implements it as `TauriSink`, which forwards to one window (plus its auxiliary windows) or all windows as the single Tauri event `ide://event`. Crates take a `SharedSink` and never depend on Tauri; tests use `NullSink`.

The UI subscribes by type, and the handler gets the narrowed variant:

```ts src/features/project-trust/index.tsx theme={null}
onIdeEvent("projectTrustChanged", (e) => {
  const project = useProject.getState().project;
  if (project?.root === e.root) useProject.setState({ project: { ...project, trusted: e.trusted } });
});
```

`onIdeEvent` returns an unsubscribe function; return it from your feature's disposer. Listening is window-scoped.

## Threads

Long work never blocks the UI thread or a Tauri command thread. Make the command `async` and move CPU-bound or blocking work onto `spawn_blocking` or rayon; honour the `CancellationToken`. Synchronous commands run on the main thread, so keep them trivial.

## Remote windows

You don't write anything special for remote development. In a remote window, `src/platform/remote.ts` routes the commands the server answers through `remote_invoke`; each `Channel` argument is replaced by `{ "$channel": id }` and its values come back tagged with the id. Events arrive as `event` notifications and are re-emitted locally. What you must do is keep the implementation in `ide-host::ops` and register it with the server's `Dispatcher` (the `d.add("search_start", ...)` lists in `crates/ide-host/src/rpc.rs` and the `ops` modules) so the server can run it. See [Remote development](https://openidea.sh/en/contribute/architecture/remote.md).

## Checklist: adding a command

1. Put the logic in the owning crate; convert errors to `IdeError` at its edge; use IPC coordinates.
2. Add the shared implementation to `ide-host::ops::<domain>` if it touches a project; register it with the remote dispatcher if remote windows need it.
3. Add the `#[tauri::command] #[specta::specta]` adapter in `src-tauri/src/commands/<domain>.rs` and list it in `collect_commands!`.
4. If it executes anything, leave it out of the restricted-mode allowlist (it is denied in untrusted projects by default).
5. `pnpm gen:bindings`, then call it with `call(commands.yourCommand(...))`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.