### Install Project Dependencies
Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/desktop/README.md
Run this command to install all required project dependencies using pnpm.
```bash
$ pnpm install
```
--------------------------------
### Start Development Server
Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/desktop/README.md
Launch the application in development mode.
```bash
$ pnpm dev
```
--------------------------------
### Define Client Capabilities
Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md
Example of the capability object sent by the client to the agent during initialization.
```ts
{
fs: { readTextFile: true, writeTextFile: true },
terminal: true, // implements createTerminal etc.
_meta: { terminal_output: true }, // wants live Bash terminal frames
auth: { terminal: true, _meta: { "terminal-auth": true, gateway: true } },
elicitation: { ... }, // structured form input
}
```
--------------------------------
### Define Agent Capabilities
Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md
Example of the capability object advertised by the claude-agent-acp.
```ts
{
protocolVersion: 1,
agentCapabilities: {
promptCapabilities: { image: true, embeddedContext: true },
mcpCapabilities: { http: true, sse: true },
loadSession: true,
sessionCapabilities: { fork: {}, list: {}, resume: {}, close: {} },
_meta: { claudeCode: { promptQueueing: true } },
},
authMethods: [ /* claude/console terminal logins, optional gateway method */ ],
}
```
--------------------------------
### Define HTML entry point
Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/server/CLAUDE.md
Use standard HTML files to import frontend scripts.
```html
Hello, world!
```
--------------------------------
### Implement server with Bun.serve
Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/server/CLAUDE.md
Configure a server with routing and optional WebSocket support using Bun's native API.
```ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
--------------------------------
### projects.create
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md
Creates a new project with the specified name and root path.
```APIDOC
## projects.create
### Description
Creates a new project. The server validates that the root path is absolute, exists, and is not already associated with an existing project.
### Signature
`projects.create({ name: string, rootPath: AbsolutePath }) → Project`
```
--------------------------------
### projects.create({ name, rootPath })
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md
Creates a new project with the specified name and root path.
```APIDOC
## projects.create({ name, rootPath })
### Description
Creates a new project.
### Parameters
- **name** (string) - The name of the project.
- **rootPath** (string) - The root directory path for the project.
### Returns
- **Project** - The created project object.
```
--------------------------------
### hello
Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md
Initializes the connection by performing authentication and client identification. This must be the first message sent after the WebSocket opens.
```APIDOC
## hello
### Description
Performs authentication and client identification. Must be the first message sent after the WebSocket opens.
### Request
- **type** (string) - Required - "hello"
- **id** (string) - Required - Unique request identifier
### Response
- **relayVersion** (string) - Relay version information
- **agent** (object) - Agent name and version
- **session** (object) - Session mode ("fresh" or "resumed") and sequence number
- **capabilities** (object) - Supported features including resume status and available channels
```
--------------------------------
### Initialize SQLite Engine Pragmas
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/db.md
Startup configuration for the SQLite database connection, including WAL mode and foreign key enforcement.
```sql
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
PRAGMA synchronous = NORMAL;
PRAGMA busy_timeout = 5000;
```
--------------------------------
### Build Application for Target Platforms
Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/desktop/README.md
Execute platform-specific build commands to package the application for Windows, macOS, or Linux.
```bash
# For windows
$ pnpm build:win
# For macOS
$ pnpm build:mac
# For Linux
$ pnpm build:linux
```
--------------------------------
### Create Workspace
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md
Initializes a new workspace as either a local directory or a git worktree.
```ts
workspaces.create({
projectId: ProjectId,
name: string,
config:
| { kind: "local" }
| { kind: "worktree"; branch?: string; baseBranch?: string },
}) → Workspace
```
--------------------------------
### Build abduco binaries
Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/desktop/resources/bin/README.md
Commands to build the abduco binaries for the host architecture or specific targets.
```bash
# From apps/desktop. Builds for the HOST arch and writes resources/bin/abduco-.
scripts/build-abduco.sh
```
```bash
TARGET=linux-arm64 scripts/build-abduco.sh # needs a matching CC/SDK
ABDUCO_VERSION=0.6 scripts/build-abduco.sh # override the pinned version
```
--------------------------------
### Initialize Electron ACP Client
Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md
Sets up a WebSocket connection and initializes the ACP client with file system capabilities and session management.
```typescript
// apps/desktop/src/main/acp-client.ts
import WebSocket from "ws";
import * as acp from "@agentclientprotocol/sdk";
import { wsAcpStream, encode } from "@sandcastle/acp-transport";
const ws = new WebSocket("wss://relay.sandcastle.dev/acp", {
headers: { Authorization: `Bearer ${token}` },
});
await new Promise(r => ws.once("open", r));
let lastSeq = 0;
const onNonAcp = (env) => {
if (env.kind === "control") handleControl(env);
// else if (env.kind === "git") ...
};
// Wrap the stream so we (a) tag each inbound ACP envelope's seq, (b) emit envelopes outbound.
const stream = wsAcpStream(ws, onNonAcp, env => { if (env.seq) lastSeq = env.seq; });
const conn = new acp.ClientSideConnection(_a => ({
async sessionUpdate(p) { sendToRenderer("session-update", p); },
async requestPermission(p) { return await askRendererForPermission(p); },
async readTextFile(p) { return { content: await fs.readFile(p.path, "utf8") }; },
async writeTextFile(p) { await fs.writeFile(p.path, p.content); return {}; },
}), stream);
// Control: hello before any ACP traffic
ws.send(encode({ kind: "control", type: "hello", id: 1, payload: { token } }));
// On reconnect: send control.resume with our lastSeq before sending any ACP frames
ws.on("open-after-reconnect", () => {
ws.send(encode({ kind: "control", type: "resume", id: 2, payload: { sessionId, since: lastSeq } }));
});
// Normal ACP usage from here:
const init = await conn.initialize({
protocolVersion: acp.PROTOCOL_VERSION,
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, _meta: { terminal_output: true } },
});
const session = await conn.newSession({ cwd: "/var/sandcastle/wsp/abc", mcpServers: [] });
```
--------------------------------
### Process Tree Comparison
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/pty-persistence.md
Visual representation of the process hierarchy difference between the current direct-spawn approach and the proposed abduco-based persistence model.
```text
Today:
Electron main ──spawn──▶ zsh ──▶ claude (one tree; quit ⇒ SIGHUP ⇒ dead)
Proposed:
Electron main ──spawn──▶ abduco client ──socket──▶ abduco server (daemon, setsid) ──▶ zsh ──▶ claude
▲
quit kills only the client; server + zsh + claude keep running
```
--------------------------------
### workspaces.create
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md
Creates a new workspace for a project, supporting either local or worktree configurations.
```APIDOC
## workspaces.create
### Description
Creates a new workspace. Supports 'local' kind (one per project) or 'worktree' kind (git-based).
### Signature
`workspaces.create({ projectId: ProjectId, name: string, config: { kind: "local" } | { kind: "worktree"; branch?: string; baseBranch?: string } }) → Workspace`
```
--------------------------------
### projects.list()
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md
Retrieves a list of all available projects.
```APIDOC
## projects.list()
### Description
Retrieves a list of all projects.
### Returns
- **Project[]** - A list of project objects.
```
--------------------------------
### workspaces.create({ projectId, name, config })
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md
Creates a new workspace within a project.
```APIDOC
## workspaces.create({ projectId, name, config })
### Description
Creates a new workspace.
### Parameters
- **projectId** (string) - The ID of the project.
- **name** (string) - The name of the workspace.
- **config** (object) - Configuration settings for the workspace.
### Returns
- **Workspace** - The created workspace object.
```
--------------------------------
### Configure Atom Caching and Lifecycle
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md
Demonstrates how to set registry-wide defaults, per-atom idle TTL, and keep-alive status to control memory usage and disposal.
```ts
// Registry-wide default (ours is 400ms — see rpc/registry.ts)
AtomRegistry.make({ defaultIdleTTL: 400 })
// Per-atom idle TTL. Finite => dispose after idle; Infinity => keepAlive; 0 => dispose immediately when idle
myAtom.pipe(Atom.setIdleTTL("5 minutes"))
myAtom.pipe(Atom.setIdleTTL(0)) // evict the instant it's unobserved
// Pin forever (survives no-subscribers). Opt out again with Atom.autoDispose
myAtom.pipe(Atom.keepAlive)
```
--------------------------------
### Run server with hot reloading
Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/server/CLAUDE.md
Execute the server script with the --hot flag for development.
```sh
bun --hot ./index.ts
```
--------------------------------
### Development Workflow Commands
Source: https://github.com/andresmarpz/sandcastle/blob/main/README.md
Standard commands for managing the monorepo development lifecycle.
```bash
pnpm install
pnpm dev # run the desktop app with HMR
pnpm quality # typecheck + biome check
pnpm build # production build
```
--------------------------------
### Pattern Match AsyncResult in UI
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md
Standard and waiting-aware pattern matching for rendering UI components based on the current state.
```ts
AsyncResult.match(result, {
onInitial: () => ,
onFailure: (f) => ,
onSuccess: (s) => ,
})
// Waiting-aware: onWaiting fires first for any waiting result AND for Initial
AsyncResult.matchWithWaiting(result, {
onWaiting: (r) => /* show stale AsyncResult.value(r) with a subtle spinner */,
onError: (e) => ...,
onDefect: (d) => ...,
onSuccess: (s) => ...,
})
```
--------------------------------
### Create React frontend component
Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/server/CLAUDE.md
Render a React component into the document body with direct CSS imports.
```tsx
import React from "react";
import { createRoot } from "react-dom/client";
// import .css files directly and it works
import './index.css';
const root = createRoot(document.body);
export default function Frontend() {
return Hello, world!
;
}
root.render();
```
--------------------------------
### Create service-backed atoms with Atom.runtime
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md
Provides atoms access to sync-engine services like databases or network layers.
```ts
const runtime = Atom.runtime(MyServicesLayer)
const thing = runtime.atom(Effect.gen(function* () { /* use services */ }))
const doThing = runtime.fn(handler, { reactivityKeys: ["things"] })
```
--------------------------------
### Package Dependency Layering
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/directories.md
Visualizes the dependency relationship between the contracts and entities packages.
```text
@sandcastle/contracts
│ uses entity types in RPC payloads
▼
@sandcastle/entities
```
--------------------------------
### Monorepo Directory Structure
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/directories.md
Visual representation of the project root, including application directories, shared packages, and configuration files.
```text
sandcastle/
├── apps/
│ ├── server/ # Bun server
│ └── desktop/ # Electron + React client
├── packages/
│ ├── entities/ # Domain entities: Project, Workspace, branded IDs
│ └── contracts/ # WS RPC group + typed errors
├── docs/
│ ├── architecture.md
│ ├── directories.md # this file
│ ├── db.md
│ └── rpc-contract.md
├── patches/ # pnpm patch-package outputs
├── package.json # root scripts; private
├── pnpm-workspace.yaml
├── pnpm-lock.yaml
├── tsconfig.json # root TS base
└── turbo.json
```
--------------------------------
### Directory structure for packages/contracts
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/directories.md
Represents the file layout for the contracts package, which defines the WebSocket RPC interface.
```text
packages/contracts/
├── src/
│ ├── index.ts
│ ├── errors.ts # typed error union (ProjectPathInvalid, WorktreeCreateFailed, …)
│ └── rpc/
│ ├── index.ts # SandcastleRpc group export
│ ├── projects.ts # projects.{list, create, rename, delete}
│ └── workspaces.ts # workspaces.{list, create, delete}
├── package.json # deps: effect, @effect/rpc, @sandcastle/entities
└── tsconfig.json
```
--------------------------------
### Abduco Session Helper Utilities
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/pty-persistence.md
Provides helpers for resolving the abduco binary path, defining the socket directory, and generating filesystem-safe session names from leafId.
```ts
import { createHash } from "node:crypto";
import os from "node:os";
import { join } from "node:path";
import process from "node:process";
import { app } from "electron";
// abduco is unix-only. Windows falls back to a plain shell (no persistence).
export const PERSISTENCE_SUPPORTED = process.platform !== "win32";
// Resolve the bundled binary. resources/** is asarUnpack'd (electron-builder.yml),
// so in prod it lives under process.resourcesPath; in dev, under the repo resources/.
export const abducoBin = (): string => {
const arch = process.arch; // "arm64" | "x64"
const name = `abduco-${process.platform}-${arch}`;
return app.isPackaged
? join(process.resourcesPath, "bin", name)
: join(app.getAppPath(), "resources", "bin", name);
};
// abduco nests sockets as ///@
// and sun_path caps at ~104 bytes on macOS. os.tmpdir() looks short but on macOS
// is /var/folders//T (~48 bytes), which overflowed once abduco appended its
// own nesting → "create-session: File name too long". Anchor at a short fixed root.
export const socketDir = (): string => "/tmp/sandcastle";
// abduco addresses sessions by NAME inside ABDUCO_SOCKET_DIR. Hash leafId to a
// short, filesystem-safe name so the socket path stays well under the limit.
export const sessionName = (leafId: string): string =>
createHash("sha256").update(leafId).digest("hex").slice(0, 16);
export const socketPath = (leafId: string): string =>
join(socketDir(), sessionName(leafId));
```
--------------------------------
### Release Tagging
Source: https://github.com/andresmarpz/sandcastle/blob/main/README.md
Commands to trigger the automated release workflow via GitHub Actions.
```bash
git tag v && git push origin v
```
--------------------------------
### Configure Atom Refetching
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md
Use withRefresh for live data polling or refreshOnWindowFocus for desktop-friendly revalidation.
```ts
// Re-run every 30s while the atom is mounted (interval/polling)
const liveStatus = Atom.make(fetchStatus).pipe(Atom.withRefresh("30 seconds"))
// On window focus (built-in)
const atom = Atom.make(fetchThing).pipe(Atom.refreshOnWindowFocus)
// equivalently swr({ revalidateOnFocus: true })
```
--------------------------------
### List Workspaces
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md
Retrieves all active workspaces for a project, ordered by creation time.
```ts
workspaces.list({ projectId: ProjectId }) → Workspace[]
```
--------------------------------
### Provide Registry Context
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md
Configures the application registry using a shared singleton within the React component tree.
```typescript
// rpc/registry.ts owns the registry; main.tsx provides it:
{app}
```
--------------------------------
### Implement persistent storage with Atom.kvs
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md
Creates a writable atom backed by localStorage or a KeyValueStore for durable local preferences.
```ts
const theme = Atom.kvs({
runtime: kvsRuntime, // Atom.runtime providing a KeyValueStore
key: "theme",
schema: Schema.Literal("light", "dark"),
defaultValue: () => "dark",
// mode: "async" to expose load as AsyncResult; default is sync
})
```
--------------------------------
### projects.list
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md
Retrieves a list of all active, non-soft-deleted projects, ordered by creation date.
```APIDOC
## projects.list
### Description
Returns all active (non-soft-deleted) projects, in creation order.
### Signature
`projects.list() → Project[]`
```
--------------------------------
### Projects RPC Methods
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md
Method signatures for project management operations.
```ts
projects.list() → Project[]
```
```ts
projects.create({
name: string,
rootPath: AbsolutePath,
}) → Project
```
```ts
projects.rename({ projectId: ProjectId, name: string }) → Project
```
```ts
projects.delete({ projectId: ProjectId }) → void
```
--------------------------------
### Signal-Driven Refresh
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md
Use signal-driven refreshes for lightweight pub/sub scenarios where reactivity keys are unnecessary.
```ts
const bumpSignal = Atom.make(0)
const atom = Atom.make(build).pipe(Atom.makeRefreshOnSignal(bumpSignal))
registry.set(bumpSignal, (n) => n + 1) // forces `atom` to rebuild
```
--------------------------------
### Run tests with Bun
Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/server/CLAUDE.md
Use the built-in test runner for unit tests.
```ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
--------------------------------
### Render SWR Atoms in React
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md
Demonstrates consuming SWR-enabled atoms using useAtomSuspense for non-blocking refreshes or useAtomValue for manual loading state handling.
```ts
// Suspends once on initial load; renders through refreshes
const user = useAtomSuspense(userAtom) // AsyncResult.Success
// Or read the raw AsyncResult and show your own "refreshing" affordance:
const result = useAtomValue(userAtom)
const data = AsyncResult.value(result) // Option, present even while waiting
const refreshing = result.waiting
```
--------------------------------
### Importing Atom and React Bindings
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md
Standard import map for accessing core reactivity primitives and React-specific hooks.
```ts
// Core (lives inside `effect` now)
import {
Atom, AsyncResult, AtomRegistry, AtomRef,
AtomRpc, AtomHttpApi, Hydration, Reactivity,
} from "effect/unstable/reactivity"
// React bindings
import {
useAtomValue, useAtom, useAtomSet, useAtomRefresh, useAtomMount,
useAtomSuspense, useAtomSubscribe, useAtomInitialValues,
useAtomRef, useAtomRefProp, useAtomRefPropValue,
RegistryContext, RegistryProvider, scheduleTask,
HydrationBoundary, make as scopedAtom,
} from "@effect/atom-react"
```
--------------------------------
### workspaces.list({ projectId })
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md
Lists all workspaces associated with a specific project.
```APIDOC
## workspaces.list({ projectId })
### Description
Retrieves a list of workspaces for a given project.
### Parameters
- **projectId** (string) - The ID of the project.
### Returns
- **Workspace[]** - A list of workspace objects.
```
--------------------------------
### workspaces.list
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md
Retrieves a list of all active workspaces for a specific project.
```APIDOC
## workspaces.list
### Description
Returns all active workspaces under the given project, in creation order. Rejects if the project is unknown or soft-deleted.
### Signature
`workspaces.list({ projectId: ProjectId }) → Workspace[]`
```
--------------------------------
### Visualize ACP Architecture
Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md
Diagram showing the communication flow from the Electron UI to the Anthropic API.
```text
Our Electron UI ──ACP via WS──▶ Our Node relay ──ACP via stdio──▶ claude-agent-acp ──Claude SDK──▶ claude (binary) ──HTTPS──▶ Anthropic API
```
--------------------------------
### Define Project Domain Model
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/architecture.md
Represents the project structure stored in the database, including path and git status metadata.
```ts
Project = {
id: ProjectId // uuid, branded
name: string // user-facing label
rootPath: AbsolutePath // absolute path on the local filesystem
isGit: boolean // probed at create-time
createdAt: IsoDateTime
updatedAt: IsoDateTime
deletedAt: IsoDateTime | null
}
```
--------------------------------
### projects.rename
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md
Updates the display name of an existing project.
```APIDOC
## projects.rename
### Description
Updates the name and updated_at timestamp for the specified project.
### Signature
`projects.rename({ projectId: ProjectId, name: string }) → Project`
```
--------------------------------
### Proposed Repository Layout
Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md
The project structure separates the Node.js relay application, the Electron desktop client, and shared ACP packages.
```text
sandcastle/
├── apps/
│ ├── api/ (this — the Node relay)
│ │ ├── src/
│ │ │ ├── index.ts # HTTP + WS server entry
│ │ │ ├── auth.ts # WS upgrade auth, JWT verification
│ │ │ ├── relay.ts # SessionBroker, frame router, child lifecycle
│ │ │ ├── transport.ts # ws ↔ acp.Stream adapters; ndjson reframer
│ │ │ ├── control.ts # sandcastle/* method handlers
│ │ │ ├── interceptors.ts # audit log, metrics, _meta tagging
│ │ │ └── env.ts # per-user env construction
│ │ └── package.json
│ └── desktop/ (Electron app)
│ ├── src/
│ │ ├── main/ # Electron main: WS connection + ACP client
│ │ ├── preload/ # IPC bridge
│ │ └── renderer/ # React UI rendering session/update notifications
│ └── package.json
└── packages/
├── acp-transport/ # shared: wsStream(ws): acp.Stream, control-method types
└── acp-types/ # shared: re-exports + sandcastle/* schemas
```
--------------------------------
### Perform Manual SWR via Registry Refresh
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md
Triggers a background refresh for tracked atoms while maintaining the stale value on screen.
```ts
// lib/prefetch.ts (paraphrased)
for (const atom of tracked) appRegistry.refresh(atom) // keepAlive keeps stale value on screen
```
--------------------------------
### projects.rename({ projectId, name })
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md
Renames an existing project.
```APIDOC
## projects.rename({ projectId, name })
### Description
Renames an existing project identified by its ID.
### Parameters
- **projectId** (string) - The ID of the project to rename.
- **name** (string) - The new name for the project.
### Returns
- **Project** - The updated project object.
```
--------------------------------
### Create PTY Session
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/pty-persistence.md
Initializes a PTY session, optionally wrapping it with abduco for persistence. The shell and environment variables are applied only during the initial creation.
```ts
const DETACH_KEY = "^g"; // neutralize accidental user-detach; we detach by killing the client
const KEEPALIVE_ON_QUIT = PERSISTENCE_SUPPORTED;
const createSession = async (sender, opts: CreateOptions) => {
const shell = opts.shell ?? defaultShell();
const cwd = opts.cwd ?? homeDir();
const { env: mcpEnv, args: mcpArgs } = registerSession(opts.id, sender.id, opts.workspaceId, shell);
const env = buildEnv({ ...opts.env, ...mcpEnv, SANDCASTLE_SESSION_ID: opts.id });
let file: string, args: string[];
if (PERSISTENCE_SUPPORTED) {
await fs.mkdir(socketDir(), { recursive: true });
env.ABDUCO_SOCKET_DIR = socketDir();
file = abducoBin();
// attach-or-create; neutralize detach key; run the shell as the session command
args = ["-e", DETACH_KEY, "-A", sessionName(opts.leafId), shell, ...shellArgs(shell), ...mcpArgs];
} else {
file = shell;
args = [...shellArgs(shell), ...mcpArgs];
}
const pty = nodePty.spawn(file, args, {
name: "xterm-256color",
cols: opts.cols ?? 80, rows: opts.rows ?? 24, cwd, env,
useConpty: process.platform === "win32",
});
const session: Session = {
id: opts.id, leafId: opts.leafId, pty, webContentsId: sender.id,
shellPid: null, // resolved lazily, see §5
};
sessions.set(opts.id, session);
watchRenderer(sender);
wireSessionEvents(session);
void resolveShellPid(session); // best-effort eager resolve for cwd/foreground/kill
};
```
--------------------------------
### Sandcastle Disk Layout
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/architecture.md
The directory structure used by Sandcastle for storing the SQLite database and git worktrees.
```text
~/.sandcastle/
├── sandcastle.db # SQLite
└── worktrees/
└── {projectId}/
└── {workspaceId}/ # `git worktree add` lives here
```
--------------------------------
### Reactivity Keys for Mutations
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md
Implement query-key style invalidation by tagging queries with keys and invalidating them upon mutation success. Prefer stable, primitive values for keys to avoid hashing issues.
```ts
// Query: declares what it depends on
Client.query("workspaces.list", { projectId }, {
reactivityKeys: ["workspaces", projectId],
timeToLive: Duration.infinity,
})
// Mutation: invalidates the same keys after it succeeds
Client.mutation("workspaces.create") // pass reactivityKeys when you set it:
registry.set(createWorkspace, { projectId, name, reactivityKeys: ["workspaces", projectId] })
```
--------------------------------
### pnpm Workspace Configuration
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/directories.md
Defines the workspace packages for the pnpm monorepo.
```yaml
# pnpm-workspace.yaml
packages:
- apps/*
- packages/*
```
--------------------------------
### Request Permission Options
Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md
Defines the permission options presented to the user when a tool requires authorization.
```typescript
[
{ kind: "allow_always", name: "Always Allow Bash(npm test:*)", optionId: "allow_always" },
{ kind: "allow_once", name: "Allow", optionId: "allow" },
{ kind: "reject_once", name: "Reject", optionId: "reject" },
]
```
--------------------------------
### resume
Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md
Reattaches to a previously active session using a session ID and sequence number.
```APIDOC
## resume
### Description
Reattaches to a previously-active session. If successful, buffered ACP frames are replayed.
### Request
- **type** (string) - Required - "resume"
- **id** (string) - Required - Unique request identifier
- **sessionId** (string) - Required - The ID of the session to resume
- **since** (number) - Required - The sequence number to resume from
### Response
- **ok** (boolean) - Success status
- **replayedThrough** (number) - Sequence number up to which frames were replayed
- **reason** (string) - Optional - Error reason if ok is false (e.g., "buffer_expired")
```
--------------------------------
### Broker Session State Machine
Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md
Visual representation of the SessionBroker lifecycle states including IDLE, ATTACHED, ACTIVE (DETACHED), DRAINING, and TORNDOWN.
```text
┌──────────┐ WS attaches, ACP initialize done
│ IDLE │ ──────────────────────────────────┐
└────┬─────┘ │
│ first prompt creates broker session ▼
▼ ┌──────────┐
┌──────────┐ WS detaches (close/error) │ ACTIVE │
│ ATTACHED │ ◀───────────────────────────▶│ (DETACHED│
└────┬─────┘ WS attaches with valid │ buffer │
│ control.resume │ fills) │
│ └────┬─────┘
│ idle for grace_period (no WS, no work) │
▼ │
┌──────────┐ ◀───────────────────────────────-─┘
│ DRAINING │ send remaining frames, flush buffer
└────┬─────┘
│
▼
┌──────────┐ SIGTERM child, free Redis stream
│ TORNDOWN │
└────┬─────┘
```
--------------------------------
### Perform atomic multi-writes with Atom.batch
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md
Use to apply multiple sync-engine changes simultaneously to prevent UI flickering during intermediate states.
```ts
Atom.batch(() => {
registry.set(a, 1)
registry.set(b, "x")
}) // derived atoms recompute once, subscribers notified once, after commit
```
--------------------------------
### Define BufferStore Interface
Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md
Interface for managing buffered envelopes to support soft-reconnects, allowing for in-memory or persistent storage implementations.
```ts
interface BufferStore {
append(sessionId: string, env: Envelope): number; // returns assigned seq
range(sessionId: string, since: number): Envelope[]; // strictly > since, in seq order
oldest(sessionId: string): number; // smallest seq still retained
trim(sessionId: string): void; // enforce caps
drop(sessionId: string): void; // teardown
}
```
--------------------------------
### Directory structure for packages/entities
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/directories.md
Represents the file layout for the domain entities package, which defines transport-agnostic data shapes.
```text
packages/entities/
├── src/
│ ├── index.ts
│ ├── ids.ts # branded IDs: ProjectId, WorkspaceId
│ ├── time.ts # IsoDateTime schema + helpers
│ ├── project.ts # Project
│ └── workspace.ts # Workspace, WorkspaceKind, WorkspaceCreateConfig
├── package.json # deps: effect
└── tsconfig.json
```
--------------------------------
### Initialize ACP Connection in Electron
Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md
Initializes the ACP client connection using the custom WebSocket stream adapter.
```typescript
const conn = new acp.ClientSideConnection(_agent => new MyClient(), wsAcpStream(ws, handleControl));
```
--------------------------------
### Define Projects Table Schema
Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/db.md
Schema definition for the projects table, including indexes for active project lookups and path uniqueness.
```sql
CREATE TABLE projects (
id TEXT PRIMARY KEY, -- ProjectId (uuid, branded)
name TEXT NOT NULL,
root_path TEXT NOT NULL, -- absolute, canonical, immutable
is_git INTEGER NOT NULL, -- bool: probed at create
created_at TEXT NOT NULL, -- ISO-8601
updated_at TEXT NOT NULL,
deleted_at TEXT
);
CREATE INDEX projects_active ON projects(deleted_at) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX projects_active_root_path ON projects(root_path) WHERE deleted_at IS NULL;
```
--------------------------------
### Resolve binary path in TypeScript
Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/desktop/resources/bin/README.md
Logic used to locate the abduco binary based on the current environment (packaged vs development).
```typescript
join(process.resourcesPath, "bin", "abduco--")
```
```typescript
join(app.getAppPath(), "resources", "bin", "abduco--")
```