---
title: IDE Integration
description: Control the Smithers IDE from workflows and tools — open files, show diffs, run terminals, ask users questions, and display overlays via smithers-ctl.
---

Smithers can talk to a running IDE instance through `smithers-ctl`, a CLI binary that the IDE installs and keeps on `PATH`. When both the binary and the right environment signals are present, workflows gain access to editor-native UI: file navigation, diff previews, terminal tabs, chat overlays, and webviews.

---

## Supported IDEs

The IDE integration is backed by `smithers-ctl`. Any IDE that ships `smithers-ctl` and sets the correct environment signals is supported. The environment detection checks five signals in order:

| Signal | Value |
|---|---|
| `SMITHERS_IDE` | `1`, `true`, or `yes` |
| `SMITHERS_CTL_ACTIVE` | `1`, `true`, or `yes` |
| `SMITHERS_SESSION_KIND` | `ide` |
| `TERM_PROGRAM` | `smithers` |
| `__CFBundleIdentifier` | contains `smithers` (macOS app bundles) |

At least one signal must be active for the environment to be considered live. The binary must also be executable on `PATH` (or at an absolute path if configured).

---

## Import

```ts
import {
  getSmithersIdeAvailability,
  isSmithersIdeAvailable,
  createSmithersIdeService,
  createSmithersIdeLayer,
  createAvailableSmithersIdeCli,
  openFile,
  openDiff,
  showOverlay,
  runTerminal,
  askUser,
  openWebview,
  SmithersIdeService,
  type SmithersIdeAvailability,
  type SmithersIdeServiceConfig,
} from "smithers-orchestrator/ide";
```

---

## Availability

### isSmithersIdeAvailable

Quick boolean check. Resolves `true` only when the binary is found and at least one environment signal is active.

```ts
const available = await isSmithersIdeAvailable();
// true | false
```

### getSmithersIdeAvailability

Full availability report with the reason and which signals fired.

```ts
const availability = await getSmithersIdeAvailability();

if (availability.available) {
  console.log("IDE found at:", availability.binaryPath);
  console.log("Active signals:", availability.signals);
} else {
  console.log("Not available:", availability.reason);
  // "binary-missing" | "environment-inactive"
}
```

### SmithersIdeAvailability

```ts
type SmithersIdeAvailability =
  | {
      available: true;
      binaryAvailable: true;
      binaryPath: string;
      environmentActive: true;
      reason: "available";
      signals: readonly string[];
    }
  | {
      available: false;
      binaryAvailable: boolean;
      binaryPath: string | null;
      environmentActive: boolean;
      reason: "binary-missing" | "environment-inactive";
      signals: readonly string[];
    };
```

`reason` distinguishes the two failure modes. `binary-missing` means `smithers-ctl` was not found on `PATH`. `environment-inactive` means the binary exists but none of the environment signals are set, which usually means the process is running outside the IDE.

---

## Configuration

All service constructors accept an optional `SmithersIdeServiceConfig`:

```ts
type SmithersIdeServiceConfig = {
  command?: string;          // Default: "smithers-ctl"
  cwd?: string;              // Default: process.cwd()
  env?: Record<string, string | undefined>;  // Default: process.env
  idleTimeoutMs?: number;    // Default: 2000
  maxOutputBytes?: number;   // Default: 200000
  timeoutMs?: number;        // Default: 10000
};
```

| Option | Default | Description |
|---|---|---|
| `command` | `"smithers-ctl"` | Binary name or absolute path |
| `cwd` | `process.cwd()` | Working directory for subprocess |
| `env` | `process.env` | Environment for subprocess |
| `idleTimeoutMs` | `2000` | Idle timeout in milliseconds |
| `maxOutputBytes` | `200000` (200 KB) | Max captured stdout/stderr |
| `timeoutMs` | `10000` | Hard timeout per command in milliseconds |

---

## Service API

### createSmithersIdeService

Returns a `SmithersIdeServiceApi` with all IDE operations as Effect-returning methods.

```ts
const service = createSmithersIdeService({ timeoutMs: 5000 });

const result = await Effect.runPromise(
  service.openFile("/src/index.ts", 42, 1),
);
```

### SmithersIdeServiceApi

```ts
type SmithersIdeServiceApi = {
  config: SmithersIdeResolvedConfig;
  detectAvailability: () => Effect.Effect<SmithersIdeAvailability>;
  openFile: (path: string, line?: number, column?: number) =>
    Effect.Effect<SmithersIdeOpenFileResult, SmithersError>;
  openDiff: (content: string) =>
    Effect.Effect<SmithersIdeOpenDiffResult, SmithersError>;
  showOverlay: (type: SmithersIdeOverlayType, options: SmithersIdeOverlayOptions) =>
    Effect.Effect<SmithersIdeOverlayResult, SmithersError>;
  runTerminal: (command: string, cwd?: string) =>
    Effect.Effect<SmithersIdeRunTerminalResult, SmithersError>;
  askUser: (prompt: string) =>
    Effect.Effect<SmithersIdeAskUserResult, SmithersError>;
  openWebview: (url: string) =>
    Effect.Effect<SmithersIdeOpenWebviewResult, SmithersError>;
};
```

---

## API Reference

### openFile

Open a file in the IDE, optionally jumping to a line and column.

```ts
openFile(path: string, line?: number, column?: number)
  => Effect.Effect<SmithersIdeOpenFileResult, SmithersError>
```

`column` requires `line`. Passing `column` without `line` fails with `INVALID_INPUT`.

```ts
// Open a file
service.openFile("/src/utils.ts");

// Jump to line 100
service.openFile("/src/utils.ts", 100);

// Jump to line 100, column 5
service.openFile("/src/utils.ts", 100, 5);
```

Invokes: `smithers-ctl open <path> [+line[:col]]`

```ts
type SmithersIdeOpenFileResult = {
  args: readonly string[];
  column: number | null;
  command: string;
  exitCode: number | null;
  line: number | null;
  opened: boolean;
  path: string;
  stderr: string;
  stdout: string;
};
```

### openDiff

Open a unified diff preview in the IDE.

```ts
openDiff(content: string)
  => Effect.Effect<SmithersIdeOpenDiffResult, SmithersError>
```

```ts
service.openDiff(`--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,4 @@
 import { foo } from "./foo";
+import { bar } from "./bar";
`);
```

Invokes: `smithers-ctl diff show --content <content>`

```ts
type SmithersIdeOpenDiffResult = {
  args: readonly string[];
  command: string;
  exitCode: number | null;
  opened: boolean;
  stderr: string;
  stdout: string;
};
```

### showOverlay

Show an overlay in the IDE.

```ts
showOverlay(type: SmithersIdeOverlayType, options: SmithersIdeOverlayOptions)
  => Effect.Effect<SmithersIdeOverlayResult, SmithersError>
```

```ts
type SmithersIdeOverlayType = "chat" | "progress" | "panel";

type SmithersIdeOverlayOptions = {
  message: string;
  title?: string;
  position?: "top" | "center" | "bottom";
  duration?: number;   // seconds
  percent?: number;    // 0–100, for "progress" type
};
```

```ts
// Progress bar at 60%
service.showOverlay("progress", {
  message: "Running tests...",
  title: "Test Suite",
  percent: 60,
  position: "bottom",
});

// Chat message
service.showOverlay("chat", {
  message: "Deployment complete.",
  duration: 5,
});
```

Invokes: `smithers-ctl overlay --type <type> --message <message> [--title ...] [--position ...] [--duration ...] [--percent ...]`

```ts
type SmithersIdeOverlayResult = {
  args: readonly string[];
  command: string;
  exitCode: number | null;
  overlayId: string | null;
  shown: boolean;
  stderr: string;
  stdout: string;
  type: SmithersIdeOverlayType;
};
```

### runTerminal

Run a command in a new IDE terminal tab.

```ts
runTerminal(command: string, cwd?: string)
  => Effect.Effect<SmithersIdeRunTerminalResult, SmithersError>
```

```ts
service.runTerminal("npm test", "/workspace/my-project");
```

Invokes: `smithers-ctl terminal [--cwd <cwd>] run <command>`

```ts
type SmithersIdeRunTerminalResult = {
  args: readonly string[];
  command: string;
  cwd: string | null;
  exitCode: number | null;
  launched: boolean;
  status: string;
  stderr: string;
  stdout: string;
  terminalCommand: string;
};
```

### askUser

Prompt the user with a chat overlay and return when the overlay is shown. This is a shim — it displays the prompt via `showOverlay("chat", ...)` and returns immediately with `status: "prompted"`. The actual user response must be collected through the IDE's chat interface.

```ts
askUser(prompt: string)
  => Effect.Effect<SmithersIdeAskUserResult, SmithersError>
```

```ts
service.askUser("Which environment should I deploy to?");
```

```ts
type SmithersIdeAskUserResult = {
  args: readonly string[];
  command: string;
  exitCode: number | null;
  overlayId: string | null;
  prompt: string;
  status: "prompted";
  stderr: string;
  stdout: string;
};
```

### openWebview

Open a URL in an IDE webview tab.

```ts
openWebview(url: string)
  => Effect.Effect<SmithersIdeOpenWebviewResult, SmithersError>
```

```ts
service.openWebview("https://smithers.dev/runs/smi_abc123");
```

Invokes: `smithers-ctl webview open <url>`

```ts
type SmithersIdeOpenWebviewResult = {
  args: readonly string[];
  command: string;
  exitCode: number | null;
  opened: boolean;
  stderr: string;
  stdout: string;
  tabId: string | null;
  url: string;
};
```

---

## Effect Layer

Use `createSmithersIdeLayer` to provide `SmithersIdeService` as an Effect Layer, then use the module-level Effect constructors (`openFile`, `openDiff`, etc.) that read from the service via `Context.Tag`.

```ts
import { Effect, Layer } from "effect";
import {
  createSmithersIdeLayer,
  openFile,
  showOverlay,
  SmithersIdeService,
} from "smithers-orchestrator/ide";

const IdeLayer = createSmithersIdeLayer({ timeoutMs: 8000 });

const program = Effect.gen(function* () {
  yield* openFile("/src/index.ts", 1);
  yield* showOverlay("chat", { message: "Opened index.ts" });
});

Effect.runPromise(Effect.provide(program, IdeLayer));
```

The module-level functions (`openFile`, `openDiff`, `showOverlay`, `runTerminal`, `askUser`, `openWebview`) each call `Effect.flatMap(SmithersIdeService, ...)` and require `SmithersIdeService` in the context.

### SmithersIdeService Tag

```ts
class SmithersIdeService extends Context.Tag("SmithersIdeService")<
  SmithersIdeService,
  SmithersIdeServiceApi
>() {}
```

---

## MCP CLI Namespace

`createSmithersIdeCli` returns a CLI object with all six IDE tools registered under the `smithers-ide` namespace. This is the integration point for MCP tool servers.

```ts
import { createSmithersIdeCli, SMITHERS_IDE_TOOL_NAMES } from "smithers-orchestrator/ide";

const cli = createSmithersIdeCli({ timeoutMs: 10_000 });
// cli is an incur Cli instance with all six tools
```

### Tool Names

```ts
const SMITHERS_IDE_TOOL_NAMES = [
  "smithers_ide_open_file",
  "smithers_ide_open_diff",
  "smithers_ide_show_overlay",
  "smithers_ide_run_terminal",
  "smithers_ide_ask_user",
  "smithers_ide_open_webview",
] as const;
```

### Tool Schemas

| Tool | Required Args | Optional Args |
|---|---|---|
| `smithers_ide_open_file` | `path: string` | `line: number`, `col: number` |
| `smithers_ide_open_diff` | `content: string` | — |
| `smithers_ide_show_overlay` | `type: "chat"\|"progress"\|"panel"`, `message: string` | `title`, `position`, `duration`, `percent` |
| `smithers_ide_run_terminal` | `cmd: string` | `cwd: string` |
| `smithers_ide_ask_user` | `prompt: string` | — |
| `smithers_ide_open_webview` | `url: string` (URL) | — |

---

## IDE-Gated CLI Commands

`createAvailableSmithersIdeCli` is a convenience wrapper that returns the CLI only when the IDE is available, and `null` otherwise. Use it to conditionally register IDE tools:

```ts
import { createAvailableSmithersIdeCli } from "smithers-orchestrator/ide";

const ideCli = await createAvailableSmithersIdeCli();
if (ideCli) {
  // Register IDE tools with your MCP server
  server.registerCli(ideCli);
}
```

---

## Error Handling

All operations throw `SmithersError` on failure.

| Code | Cause |
|---|---|
| `INVALID_INPUT` | Empty `path`, `content`, `command`, or `url`; or `column` provided without `line` |
| `PROCESS_SPAWN_FAILED` | `smithers-ctl` not found on `PATH` or not executable |
| `TOOL_COMMAND_FAILED` | `smithers-ctl` exited with a non-zero exit code |

`PROCESS_SPAWN_FAILED` with `ENOENT` produces a human-readable message: `smithers-ctl is not installed or not on PATH`.

---

## Full Example

```ts
import { Effect } from "effect";
import {
  getSmithersIdeAvailability,
  createSmithersIdeService,
} from "smithers-orchestrator/ide";

async function runIdeWorkflow() {
  const availability = await getSmithersIdeAvailability();

  if (!availability.available) {
    console.log(`IDE not available: ${availability.reason}`);
    return;
  }

  const service = createSmithersIdeService();

  await Effect.runPromise(
    Effect.gen(function* () {
      // Open the entrypoint
      yield* service.openFile("/workspace/src/index.ts", 1);

      // Show a progress overlay while working
      yield* service.showOverlay("progress", {
        message: "Analyzing codebase...",
        title: "Smithers",
        percent: 0,
        position: "bottom",
      });

      // Run tests in a new terminal tab
      yield* service.runTerminal("npm test", "/workspace");

      // Open a diff when done
      yield* service.openDiff(generatedDiff);

      // Ask the user what to do next
      yield* service.askUser("Tests passed. Should I open a PR?");
    }),
  );
}
```
