#!/usr/bin/env node
var __defProp = Object.defineProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};

// bin/cli.ts
import { program } from "commander";
import { networkInterfaces } from "os";
import { spawn } from "child_process";
import * as p from "@clack/prompts";

// src/index.ts
import express from "express";
import { createServer } from "http";
import { WebSocketServer } from "ws";
import path10 from "path";
import { fileURLToPath as fileURLToPath2 } from "url";
import { createExpressMiddleware } from "@trpc/server/adapters/express";
import { applyWSSHandler } from "@trpc/server/adapters/ws";
import { muxManager as muxManager3 } from "@bytespell/amux/streams/manager";
import { setVerbose, debug } from "@bytespell/amux/lib/logger";

// src/db/index.ts
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";

// src/lib/paths.ts
import path from "path";
import os from "os";
import fs from "fs";
function getDataDir() {
  const home = os.homedir();
  let dataDir;
  switch (process.platform) {
    case "darwin":
      dataDir = path.join(home, "Library", "Application Support", "shella");
      break;
    case "win32":
      dataDir = path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "shella");
      break;
    default:
      dataDir = path.join(process.env.XDG_DATA_HOME || path.join(home, ".local", "share"), "shella");
  }
  return dataDir;
}
function isMockMode() {
  return process.env.SHELLA_MOCK_MODE === "true";
}
function getShellaDbPath() {
  const filename = isMockMode() ? "shella-ui.mock.db" : "shella-ui.db";
  return path.join(getDataDir(), filename);
}
function ensureDir(dir) {
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
}

// src/db/schema.ts
var schema_exports = {};
__export(schema_exports, {
  panes: () => panes,
  streamConfigs: () => streamConfigs,
  streams: () => streams,
  uiState: () => uiState,
  windows: () => windows
});
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { sql } from "drizzle-orm";
var streamConfigs = sqliteTable("stream_configs", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  /** Driver type for routing (e.g., 'acp', 'pty', or custom plugin types) */
  driverType: text("driver_type").notNull().default("acp"),
  command: text("command").notNull(),
  args: text("args", { mode: "json" }).$type().default([]),
  env: text("env", { mode: "json" }).$type().default({}),
  createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
});
var streams = sqliteTable("streams", {
  id: text("id").primaryKey(),
  directory: text("directory").notNull(),
  streamConfigId: text("stream_config_id").notNull().references(() => streamConfigs.id),
  /** Stream type determined by the driver at creation time. Built-in: 'acp', 'pty' */
  streamType: text("stream_type").notNull(),
  /** Generic driver state (e.g., ACP's session ID for resumption) */
  driverState: text("driver_state", { mode: "json" }).$type(),
  /** Title from stream (via session_info_update for ACP) */
  title: text("title"),
  model: text("model"),
  mode: text("mode"),
  createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
});
var windows = sqliteTable("windows", {
  id: text("id").primaryKey(),
  title: text("title").notNull(),
  hasCustomTitle: integer("has_custom_title", { mode: "boolean" }).notNull().default(false),
  /** JSON-serialized LayoutNode tree */
  layout: text("layout", { mode: "json" }).$type().notNull(),
  activePaneId: text("active_pane_id").notNull(),
  createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`),
  lastAccessedAt: integer("last_accessed_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
});
var panes = sqliteTable("panes", {
  id: text("id").primaryKey(),
  windowId: text("window_id").notNull().references(() => windows.id, { onDelete: "cascade" }),
  /** Static content (browser URL, etc.). Null when no content. */
  content: text("content", { mode: "json" }).$type(),
  /** Live event stream (amux stream). Null when no stream. */
  eventStream: text("event_stream", { mode: "json" }).$type(),
  /** Renderer ID (e.g., 'builtin:terminal', 'project:custom-viewer'). Null = use default. */
  rendererId: text("renderer_id"),
  createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
});
var uiState = sqliteTable("ui_state", {
  key: text("key").primaryKey(),
  value: text("value").notNull()
});

// src/db/index.ts
ensureDir(getDataDir());
var sqlite = new Database(getShellaDbPath());
sqlite.pragma("journal_mode = WAL");
sqlite.pragma("foreign_keys = ON");
var db = drizzle(sqlite, { schema: schema_exports });
sqlite.exec(`
  -- Stream tables (from amux)
  CREATE TABLE IF NOT EXISTS stream_configs (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    driver_type TEXT NOT NULL DEFAULT 'acp',
    command TEXT NOT NULL,
    args TEXT DEFAULT '[]',
    env TEXT DEFAULT '{}',
    created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
  );

  CREATE TABLE IF NOT EXISTS streams (
    id TEXT PRIMARY KEY,
    directory TEXT NOT NULL,
    stream_config_id TEXT NOT NULL REFERENCES stream_configs(id),
    stream_type TEXT NOT NULL,
    driver_state TEXT,
    title TEXT,
    model TEXT,
    mode TEXT,
    created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
  );

  -- UI tables
  CREATE TABLE IF NOT EXISTS windows (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    has_custom_title INTEGER NOT NULL DEFAULT 0,
    layout TEXT NOT NULL,
    active_pane_id TEXT NOT NULL,
    created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
    last_accessed_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
  );

  CREATE TABLE IF NOT EXISTS panes (
    id TEXT PRIMARY KEY,
    window_id TEXT NOT NULL REFERENCES windows(id) ON DELETE CASCADE,
    content TEXT,
    event_stream TEXT,
    renderer_id TEXT,
    created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
  );

  CREATE TABLE IF NOT EXISTS ui_state (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL
  );
`);
try {
  const configInfo = sqlite.prepare(`PRAGMA table_info(stream_configs)`).all();
  const hasDriverType = configInfo.some((col) => col.name === "driver_type");
  if (!hasDriverType) {
    console.log("[db] Adding driver_type column to stream_configs...");
    sqlite.exec(`ALTER TABLE stream_configs ADD COLUMN driver_type TEXT NOT NULL DEFAULT 'acp'`);
    sqlite.exec(`UPDATE stream_configs SET driver_type = 'pty' WHERE command = '__shell__'`);
    sqlite.exec(`UPDATE stream_configs SET driver_type = 'mock' WHERE command = '__mock__'`);
    console.log("[db] driver_type migration complete");
  }
} catch (e) {
}
try {
  const tableInfo = sqlite.prepare(`PRAGMA table_info(panes)`).all();
  const hasProcessColumn = tableInfo.some((col) => col.name === "process");
  const hasEventStreamColumn = tableInfo.some((col) => col.name === "event_stream");
  if (hasProcessColumn && !hasEventStreamColumn) {
    console.log("[db] Migrating panes.process to panes.event_stream...");
    sqlite.exec(`ALTER TABLE panes ADD COLUMN event_stream TEXT`);
    const panesWithProcess = sqlite.prepare(`SELECT id, process FROM panes WHERE process IS NOT NULL`).all();
    for (const pane of panesWithProcess) {
      try {
        const process2 = JSON.parse(pane.process);
        if (process2.type === "session" && process2.sessionId) {
          const eventStream = JSON.stringify({ type: "acp", streamId: process2.sessionId });
          sqlite.prepare(`UPDATE panes SET event_stream = ? WHERE id = ?`).run(eventStream, pane.id);
        }
      } catch (e) {
      }
    }
    console.log("[db] Migration complete, migrated", panesWithProcess.length, "panes");
  }
} catch (e) {
}

// src/lib/configDiscovery.ts
import { execSync } from "child_process";
import { randomUUID } from "crypto";
function commandExists(cmd) {
  try {
    execSync(`which ${cmd}`, { stdio: "ignore" });
    return true;
  } catch {
    return false;
  }
}
var claudeDiscoverer = {
  name: "claude",
  discover() {
    if (!commandExists("claude")) {
      return null;
    }
    return {
      id: randomUUID(),
      name: "claude",
      driverType: "acp",
      command: "npx",
      args: ["@zed-industries/claude-code-acp"],
      env: {},
      createdAt: /* @__PURE__ */ new Date()
    };
  }
};
var codexDiscoverer = {
  name: "codex",
  discover() {
    if (!commandExists("codex")) {
      return null;
    }
    return {
      id: randomUUID(),
      name: "codex",
      driverType: "acp",
      command: "npx",
      args: ["@zed-industries/codex-acp"],
      env: {},
      createdAt: /* @__PURE__ */ new Date()
    };
  }
};
var geminiDiscoverer = {
  name: "gemini",
  discover() {
    if (!commandExists("gemini")) {
      return null;
    }
    return {
      id: randomUUID(),
      name: "gemini",
      driverType: "acp",
      command: "gemini",
      args: ["--experimental-acp"],
      env: {},
      createdAt: /* @__PURE__ */ new Date()
    };
  }
};
var openCodeDiscoverer = {
  name: "opencode",
  discover() {
    if (!commandExists("opencode")) {
      return null;
    }
    return {
      id: randomUUID(),
      name: "opencode",
      driverType: "acp",
      command: "opencode",
      args: ["acp"],
      env: {},
      createdAt: /* @__PURE__ */ new Date()
    };
  }
};
var shellDiscoverer = {
  name: "Shell",
  discover() {
    return {
      id: "shell",
      name: "shell",
      driverType: "pty",
      command: "",
      // PTY driver ignores command, spawns user's $SHELL
      args: [],
      env: {},
      createdAt: /* @__PURE__ */ new Date()
    };
  }
};
var builtInDiscoverers = [
  claudeDiscoverer,
  codexDiscoverer,
  geminiDiscoverer,
  openCodeDiscoverer,
  shellDiscoverer
];
function discoverAndRegisterConfigs(discoverers) {
  const existing = db.select().from(streamConfigs).all();
  let added = 0;
  for (const discoverer of discoverers) {
    const config = discoverer.discover();
    if (!config) {
      continue;
    }
    const alreadyExists = existing.some(
      (e) => e.command === config.command && JSON.stringify(e.args) === JSON.stringify(config.args)
    );
    if (!alreadyExists) {
      db.insert(streamConfigs).values({
        id: config.id,
        name: config.name,
        driverType: config.driverType,
        command: config.command,
        args: config.args,
        env: config.env,
        createdAt: config.createdAt
      }).run();
      console.log(`[config] Discovered: ${config.name}`);
      added++;
    }
  }
  if (added > 0) {
    console.log(`[config] ${added} new stream config(s) registered`);
  }
}

// src/lib/driverWatcher.ts
import chokidar from "chokidar";
import * as path3 from "path";
import * as os2 from "os";
import * as fs3 from "fs/promises";
import { EventEmitter } from "events";

// src/lib/driverCompiler.ts
import { build } from "esbuild";
import * as fs2 from "fs/promises";
import * as path2 from "path";
import { fileURLToPath } from "url";
var __dirname = path2.dirname(fileURLToPath(import.meta.url));
async function compileDriver(pluginDir) {
  const serverPath = path2.join(pluginDir, "server.ts");
  try {
    await fs2.access(serverPath);
  } catch {
    return null;
  }
  const metaPath = path2.join(pluginDir, "meta.json");
  let meta;
  try {
    const metaContent = await fs2.readFile(metaPath, "utf-8");
    meta = JSON.parse(metaContent);
  } catch (err) {
    throw new Error(
      `Failed to read meta.json for ${pluginDir}: ${err instanceof Error ? err.message : String(err)}`
    );
  }
  if (!meta.id || typeof meta.id !== "string") {
    throw new Error(`Plugin in ${pluginDir}: meta.json requires "id" field`);
  }
  if (!meta.name || typeof meta.name !== "string") {
    throw new Error(`Plugin in ${pluginDir}: meta.json requires "name" field`);
  }
  const outDir = path2.join(pluginDir, ".shella");
  const outFile = path2.join(outDir, "server.mjs");
  await fs2.mkdir(outDir, { recursive: true });
  const pkgPath = path2.join(pluginDir, "package.json");
  let hasPackageJson = false;
  try {
    await fs2.access(pkgPath);
    hasPackageJson = true;
  } catch {
  }
  const shellaRoot = path2.resolve(__dirname, "../..");
  const sdkPath = path2.resolve(shellaRoot, "../sdk/dist");
  await build({
    entryPoints: [serverPath],
    bundle: true,
    write: true,
    format: "esm",
    platform: "node",
    target: "node20",
    outfile: outFile,
    alias: {
      "@bytespell/shella-sdk": path2.join(sdkPath, "index.js"),
      "@bytespell/shella-sdk/server": path2.join(sdkPath, "server.js")
    },
    // If plugin has package.json, look for node_modules
    nodePaths: hasPackageJson ? [path2.join(pluginDir, "node_modules")] : [],
    minify: false
  });
  return {
    id: meta.id,
    name: meta.name,
    filePath: outFile,
    pluginDir
  };
}

// src/lib/driverWatcher.ts
import { muxManager } from "@bytespell/amux/streams/manager";
import { eq } from "drizzle-orm";
var DriverEventEmitter = class extends EventEmitter {
  emit(event, data) {
    return super.emit(event, data);
  }
  on(event, listener) {
    return super.on(event, listener);
  }
  off(event, listener) {
    return super.off(event, listener);
  }
};
var loadedDrivers = /* @__PURE__ */ new Map();
async function loadDriver(compiled) {
  const entry = loadedDrivers.get(compiled.pluginDir);
  const version = (entry?.version ?? 0) + 1;
  const url = `file://${compiled.filePath}?v=${version}`;
  const module = await import(
    /* @vite-ignore */
    url
  );
  const driver = module.default;
  if (!driver || typeof driver.start !== "function") {
    throw new Error(`Plugin driver must export default with start() method`);
  }
  driver.type = compiled.id;
  loadedDrivers.set(compiled.pluginDir, { id: compiled.id, version });
  return driver;
}
function ensureStreamConfig(compiled) {
  const existing = db.select().from(streamConfigs).where(eq(streamConfigs.id, compiled.id)).get();
  if (!existing) {
    db.insert(streamConfigs).values({
      id: compiled.id,
      name: compiled.name,
      driverType: compiled.id,
      // Driver type matches plugin id
      command: "",
      // Plugin drivers don't spawn processes
      args: [],
      env: {},
      createdAt: /* @__PURE__ */ new Date()
    }).run();
  } else if (existing.name !== compiled.name) {
    db.update(streamConfigs).set({ name: compiled.name }).where(eq(streamConfigs.id, compiled.id)).run();
  }
}
function removeStreamConfig(id) {
  db.delete(streamConfigs).where(eq(streamConfigs.id, id)).run();
}
function watchDrivers(emitter) {
  const userPath = path3.join(os2.homedir(), ".shella", "renderers");
  const pendingCompiles = /* @__PURE__ */ new Map();
  const watcher = chokidar.watch(userPath, {
    ignoreInitial: false,
    persistent: true,
    awaitWriteFinish: {
      stabilityThreshold: 150,
      pollInterval: 50
    },
    depth: 3
    // Watch inside plugin folders
  });
  const getPluginDir = (filePath) => {
    const relative3 = path3.relative(userPath, filePath);
    const parts = relative3.split(path3.sep);
    if (parts.length >= 1 && parts[0]) {
      return path3.join(userPath, parts[0]);
    }
    return null;
  };
  const isServerFile = (filePath) => {
    const relative3 = path3.relative(userPath, filePath);
    const parts = relative3.split(path3.sep);
    return parts.length >= 2 && (parts[1] === "server.ts" || parts[1]?.startsWith("server/") || filePath.includes("/server/"));
  };
  const scheduleCompile = (pluginDir) => {
    if (pendingCompiles.has(pluginDir)) {
      clearTimeout(pendingCompiles.get(pluginDir));
    }
    pendingCompiles.set(
      pluginDir,
      setTimeout(async () => {
        pendingCompiles.delete(pluginDir);
        await handleDriverChange(pluginDir);
      }, 150)
    );
  };
  const handleDriverChange = async (pluginDir) => {
    try {
      const compiled = await compileDriver(pluginDir);
      if (!compiled) return;
      const existing = loadedDrivers.get(pluginDir);
      if (existing) {
        muxManager.unregisterDriver(existing.id);
      }
      const driver = await loadDriver(compiled);
      muxManager.registerDriver(driver);
      ensureStreamConfig(compiled);
      if (!existing) {
        console.log(`[drivers] Added: ${compiled.id} (${compiled.name})`);
        emitter.emit("driver:added", { id: compiled.id, name: compiled.name });
      } else {
        console.log(`[drivers] Updated: ${compiled.id} (${compiled.name})`);
        emitter.emit("driver:updated", { id: compiled.id, name: compiled.name });
      }
    } catch (err) {
      console.error(`[drivers] Error compiling ${pluginDir}:`, err);
      emitter.emit("driver:error", {
        pluginDir,
        error: err instanceof Error ? err : new Error(String(err))
      });
    }
  };
  watcher.on("add", (filePath) => {
    if (isServerFile(filePath)) {
      const pluginDir = getPluginDir(filePath);
      if (pluginDir) scheduleCompile(pluginDir);
    }
  });
  watcher.on("change", (filePath) => {
    if (isServerFile(filePath)) {
      const pluginDir = getPluginDir(filePath);
      if (pluginDir) scheduleCompile(pluginDir);
    }
  });
  watcher.on("unlink", (filePath) => {
    if (filePath.endsWith("server.ts")) {
      const pluginDir = getPluginDir(filePath);
      if (pluginDir) {
        const entry = loadedDrivers.get(pluginDir);
        if (entry) {
          muxManager.unregisterDriver(entry.id);
          removeStreamConfig(entry.id);
          loadedDrivers.delete(pluginDir);
          console.log(`[drivers] Removed: ${entry.id}`);
          emitter.emit("driver:removed", { id: entry.id });
        }
      }
    }
  });
  watcher.on("error", (err) => {
    console.error("[drivers] Watcher error:", err);
  });
  return () => {
    for (const timeout of pendingCompiles.values()) {
      clearTimeout(timeout);
    }
    pendingCompiles.clear();
    watcher.close();
  };
}
async function scanDrivers() {
  const userPath = path3.join(os2.homedir(), ".shella", "renderers");
  try {
    const entries = await fs3.readdir(userPath, { withFileTypes: true });
    for (const entry of entries) {
      if (!entry.isDirectory()) continue;
      const pluginDir = path3.join(userPath, entry.name);
      try {
        const compiled = await compileDriver(pluginDir);
        if (compiled) {
          const driver = await loadDriver(compiled);
          muxManager.registerDriver(driver);
          ensureStreamConfig(compiled);
          console.log(`[drivers] Loaded: ${compiled.id} (${compiled.name})`);
        }
      } catch (err) {
        console.error(`[drivers] Error loading ${pluginDir}:`, err);
      }
    }
  } catch {
  }
}

// src/amuxBridge.ts
import { randomUUID as randomUUID2 } from "crypto";
import path5 from "path";
import fs5 from "fs";
import { eq as eq2, inArray } from "drizzle-orm";

// src/lib/files.ts
import * as fs4 from "fs/promises";
import * as path4 from "path";
import { glob } from "glob";
async function listFilesForAutocomplete(workingDir, partialPath, limit) {
  const normalized = partialPath.startsWith("./") ? partialPath.slice(2) : partialPath;
  if (normalized.includes("/")) {
    return listFilesPrefix(workingDir, normalized, limit);
  }
  return listFilesFuzzy(workingDir, normalized, limit);
}
async function listFilesPrefix(workingDir, partialPath, limit) {
  const lastSlash = partialPath.lastIndexOf("/");
  const dirPart = lastSlash >= 0 ? partialPath.slice(0, lastSlash) : "";
  const prefix = lastSlash >= 0 ? partialPath.slice(lastSlash + 1) : partialPath;
  const targetDir = path4.resolve(workingDir, dirPart);
  const normalizedWorkingDir = path4.resolve(workingDir);
  const normalizedTargetDir = path4.resolve(targetDir);
  if (!normalizedTargetDir.startsWith(normalizedWorkingDir)) {
    return [];
  }
  try {
    const entries = await fs4.readdir(targetDir, { withFileTypes: true });
    const matching = entries.filter((e) => !e.name.startsWith(".")).filter((e) => prefix === "" || e.name.toLowerCase().startsWith(prefix.toLowerCase())).slice(0, limit * 2).map((e) => ({
      name: e.name,
      path: dirPart ? `${dirPart}/${e.name}` : e.name,
      isDirectory: e.isDirectory()
    }));
    matching.sort((a, b) => {
      if (a.isDirectory !== b.isDirectory) {
        return a.isDirectory ? -1 : 1;
      }
      return a.name.localeCompare(b.name);
    });
    return matching.slice(0, limit);
  } catch {
    return [];
  }
}
async function listFilesFuzzy(workingDir, query, limit) {
  if (!query.trim()) return [];
  try {
    const pattern = `{*${query}*,**/*${query}*}`;
    const matches = await glob(pattern, {
      cwd: workingDir,
      nodir: false,
      dot: false,
      // Skip hidden files
      ignore: ["**/node_modules/**", "**/.git/**"],
      maxDepth: 10
    });
    const results = [];
    for (const match of matches.slice(0, limit * 2)) {
      try {
        const fullPath = path4.join(workingDir, match);
        const stat2 = await fs4.stat(fullPath);
        results.push({
          name: path4.basename(match),
          path: match,
          isDirectory: stat2.isDirectory()
        });
      } catch {
      }
    }
    results.sort((a, b) => {
      if (a.isDirectory !== b.isDirectory) {
        return a.isDirectory ? -1 : 1;
      }
      const aDepth = a.path.split("/").length;
      const bDepth = b.path.split("/").length;
      if (aDepth !== bDepth) {
        return aDepth - bDepth;
      }
      return a.path.localeCompare(b.path);
    });
    return results.slice(0, limit);
  } catch {
    return [];
  }
}

// src/amuxBridge.ts
import { muxManager as muxManager2 } from "@bytespell/amux/streams/manager";
import { isAcpEvent } from "@bytespell/amux/types";
function getStreamStorageDir(streamId) {
  return path5.join(getDataDir(), "streams", streamId);
}
function generateTitleFromMessage(message) {
  const cleaned = message.replace(/@[\w./~-]+/g, "").replace(/\s+/g, " ").trim();
  const firstLine = cleaned.split("\n")[0] || cleaned;
  if (firstLine.length <= 50) return firstLine;
  const truncated = firstLine.slice(0, 50);
  const lastSpace = truncated.lastIndexOf(" ");
  return lastSpace > 20 ? truncated.slice(0, lastSpace) + "..." : truncated + "...";
}
function createAmuxBridge() {
  const bridge = {
    async getStreams(ids) {
      if (ids.length === 0) return [];
      return db.select().from(streams).where(inArray(streams.id, ids)).all();
    },
    async getStream(id) {
      return db.select().from(streams).where(eq2(streams.id, id)).get() ?? null;
    },
    async createStream(params) {
      let configId = params.streamConfigId;
      if (!configId) {
        const lastUsedRow = db.select().from(uiState).where(eq2(uiState.key, "last_used_stream_config_id")).get();
        if (lastUsedRow?.value) {
          const config2 = db.select().from(streamConfigs).where(eq2(streamConfigs.id, lastUsedRow.value)).get();
          if (config2) configId = config2.id;
        }
        if (!configId) {
          const firstConfig = db.select().from(streamConfigs).get();
          if (!firstConfig) {
            throw new Error("No stream configs available");
          }
          configId = firstConfig.id;
        }
      } else {
        db.insert(uiState).values({ key: "last_used_stream_config_id", value: configId }).onConflictDoUpdate({ target: uiState.key, set: { value: configId } }).run();
      }
      const id = params.id ?? randomUUID2();
      const now = /* @__PURE__ */ new Date();
      const config = db.select().from(streamConfigs).where(eq2(streamConfigs.id, configId)).get();
      const streamType = config?.driverType === "pty" ? "pty" : "acp";
      const initialTitle = streamType === "pty" ? params.directory.split("/").pop() || null : null;
      db.insert(streams).values({
        id,
        directory: params.directory,
        streamConfigId: configId,
        streamType,
        driverState: null,
        title: initialTitle,
        model: null,
        mode: null,
        createdAt: now
      }).run();
      return db.select().from(streams).where(eq2(streams.id, id)).get();
    },
    async deleteStream(id) {
      await muxManager2.stopForStream(id);
      const storageDir = getStreamStorageDir(id);
      try {
        if (fs5.existsSync(storageDir)) {
          fs5.rmSync(storageDir, { recursive: true });
        }
      } catch {
      }
      db.delete(streams).where(eq2(streams.id, id)).run();
    },
    async getStreamConfigs() {
      return db.select().from(streamConfigs).all();
    },
    async startStream(streamId) {
      const dbStream = db.select().from(streams).where(eq2(streams.id, streamId)).get();
      if (!dbStream) throw new Error(`Stream ${streamId} not found`);
      const dbConfig = db.select().from(streamConfigs).where(eq2(streamConfigs.id, dbStream.streamConfigId)).get();
      if (!dbConfig) throw new Error(`Stream config ${dbStream.streamConfigId} not found`);
      const storageDir = getStreamStorageDir(streamId);
      const emit = (event) => {
        if (isAcpEvent(event) && event.sessionUpdate === "session_info_update") {
          const title = event.title;
          if (title !== void 0) {
            db.update(streams).set({ title }).where(eq2(streams.id, streamId)).run();
          }
        }
      };
      const handle = await muxManager2.startStream({
        streamId,
        driverType: dbConfig.driverType,
        config: {
          id: dbConfig.id,
          name: dbConfig.name,
          command: dbConfig.command,
          args: dbConfig.args ?? [],
          env: dbConfig.env ?? {}
        },
        cwd: dbStream.directory,
        restoredState: dbStream.driverState,
        emit,
        storageDir
      });
      const driverState = muxManager2.getDriverState(streamId);
      if (driverState !== void 0) {
        db.update(streams).set({ driverState }).where(eq2(streams.id, streamId)).run();
      }
      const replayData = muxManager2.getReplayData(streamId);
      const replayEvents = Array.isArray(replayData) ? replayData : [];
      const scrollback = typeof replayData === "string" ? replayData : muxManager2.getTerminalScrollback(streamId);
      return {
        acpSessionId: handle.stream.acpSessionId,
        replayEvents,
        scrollback,
        models: handle.stream.models,
        modes: handle.stream.modes
      };
    },
    async stopStream(streamId) {
      const driverState = muxManager2.getDriverState(streamId);
      if (driverState !== void 0) {
        db.update(streams).set({ driverState }).where(eq2(streams.id, streamId)).run();
      }
      await muxManager2.stopForStream(streamId);
    },
    async input(streamId, message) {
      const stream = db.select().from(streams).where(eq2(streams.id, streamId)).get();
      if (!stream) throw new Error(`Stream ${streamId} not found`);
      if (!stream.title) {
        const title = generateTitleFromMessage(message);
        if (title) {
          db.update(streams).set({ title }).where(eq2(streams.id, streamId)).run();
        }
      }
      await muxManager2.input(streamId, message);
    },
    async cancel(streamId) {
      await muxManager2.cancel(streamId);
    },
    async setMode(streamId, modeId) {
      await muxManager2.setMode(streamId, modeId);
    },
    async setModel(streamId, modelId) {
      await muxManager2.setModel(streamId, modelId);
    },
    respondPermission(streamId, requestId, optionId) {
      muxManager2.respondPermission(streamId, requestId, optionId);
    },
    subscribeToStream(streamId, callback) {
      const handler = (event) => {
        if (event.streamId === streamId) {
          callback(event);
        }
      };
      muxManager2.on("update", handler);
      return () => muxManager2.off("update", handler);
    },
    async listFilesForAutocomplete(streamId, partialPath, limit) {
      const stream = db.select().from(streams).where(eq2(streams.id, streamId)).get();
      if (!stream) throw new Error(`Stream ${streamId} not found`);
      return listFilesForAutocomplete(stream.directory, partialPath, limit);
    },
    // Terminal operations
    terminalWrite(streamId, data) {
      muxManager2.terminalWrite(streamId, data);
    },
    terminalResize(streamId, cols, rows) {
      muxManager2.terminalResize(streamId, cols, rows);
    },
    getTerminalScrollback(streamId) {
      return muxManager2.getTerminalScrollback(streamId);
    },
    isTerminalStream(streamId) {
      return muxManager2.isTerminalStream(streamId);
    },
    updateStreamTitle(streamId, title) {
      db.update(streams).set({ title }).where(eq2(streams.id, streamId)).run();
    }
  };
  return bridge;
}

// src/trpc/trpc.ts
import { initTRPC } from "@trpc/server";
var t = initTRPC.create();
var router = t.router;
var publicProcedure = t.procedure;

// src/trpc/windows.ts
import { z } from "zod";
import { randomUUID as randomUUID3 } from "crypto";
import { eq as eq3 } from "drizzle-orm";

// client/lib/layout.ts
function createLeaf(paneId) {
  return { type: "leaf", paneId };
}
function createBranch(direction, first, second, ratio = 50) {
  return { type: "branch", direction, ratio, first, second };
}
function getAllPaneIds(node) {
  if (node.type === "leaf") {
    return [node.paneId];
  }
  return [...getAllPaneIds(node.first), ...getAllPaneIds(node.second)];
}
function findPanePath(node, paneId, path11 = []) {
  if (node.type === "leaf") {
    return node.paneId === paneId ? path11 : null;
  }
  const firstPath = findPanePath(node.first, paneId, [...path11, "first"]);
  if (firstPath) return firstPath;
  return findPanePath(node.second, paneId, [...path11, "second"]);
}
function replacePaneWithNode(root, paneId, newNode) {
  if (root.type === "leaf") {
    return root.paneId === paneId ? newNode : root;
  }
  return {
    ...root,
    first: replacePaneWithNode(root.first, paneId, newNode),
    second: replacePaneWithNode(root.second, paneId, newNode)
  };
}
function splitPane(root, paneId, newPaneId, direction) {
  const originalLeaf = createLeaf(paneId);
  const newLeaf = createLeaf(newPaneId);
  const branch = createBranch(direction, originalLeaf, newLeaf);
  return replacePaneWithNode(root, paneId, branch);
}
function removePane(root, paneId) {
  if (root.type === "leaf") {
    return root.paneId === paneId ? null : root;
  }
  if (root.first.type === "leaf" && root.first.paneId === paneId) {
    return root.second;
  }
  if (root.second.type === "leaf" && root.second.paneId === paneId) {
    return root.first;
  }
  const newFirst = removePane(root.first, paneId);
  const newSecond = removePane(root.second, paneId);
  if (newFirst === null) return newSecond;
  if (newSecond === null) return newFirst;
  if (newFirst !== root.first || newSecond !== root.second) {
    return { ...root, first: newFirst, second: newSecond };
  }
  return root;
}
function updateRatio(root, path11, newRatio) {
  if (path11.length === 0) {
    if (root.type === "branch") {
      return { ...root, ratio: Math.max(10, Math.min(90, newRatio)) };
    }
    return root;
  }
  if (root.type === "leaf") return root;
  const [next, ...rest] = path11;
  if (next === "first") {
    return { ...root, first: updateRatio(root.first, rest, newRatio) };
  } else {
    return { ...root, second: updateRatio(root.second, rest, newRatio) };
  }
}
function getAdjacentPane(root, currentPaneId, direction) {
  const path11 = findPanePath(root, currentPaneId);
  if (!path11) return null;
  let node = root;
  const pathNodes = [];
  for (const step of path11) {
    if (node.type === "branch") {
      pathNodes.push({ node, childSide: step });
      node = step === "first" ? node.first : node.second;
    }
  }
  for (let i = pathNodes.length - 1; i >= 0; i--) {
    const { node: branch, childSide } = pathNodes[i];
    const isHorizontal = branch.direction === "horizontal";
    const isVertical = branch.direction === "vertical";
    const canMove = direction === "left" && isHorizontal && childSide === "second" || direction === "right" && isHorizontal && childSide === "first" || direction === "up" && isVertical && childSide === "second" || direction === "down" && isVertical && childSide === "first";
    if (canMove) {
      const otherChild = childSide === "first" ? branch.second : branch.first;
      return getFirstPaneInDirection(otherChild, direction);
    }
  }
  return null;
}
function getFirstPaneInDirection(node, fromDirection) {
  if (node.type === "leaf") {
    return node.paneId;
  }
  const isHorizontal = node.direction === "horizontal";
  if (fromDirection === "left" || fromDirection === "up") {
    const targetChild = isHorizontal ? fromDirection === "left" ? node.first : node.first : fromDirection === "up" ? node.first : node.first;
    return getFirstPaneInDirection(targetChild, fromDirection);
  } else {
    const targetChild = isHorizontal ? fromDirection === "right" ? node.second : node.second : fromDirection === "down" ? node.second : node.second;
    return getFirstPaneInDirection(targetChild, fromDirection);
  }
}

// src/trpc/windows.ts
var amux;
function setAmuxBridge(bridge) {
  amux = bridge;
}
function generateTitle(existingCount, directory) {
  const dirName = directory.split("/").pop();
  if (dirName && dirName !== "~" && dirName !== "") {
    return dirName;
  }
  return `Window ${existingCount + 1}`;
}
var windowsRouter = router({
  /**
   * List all windows with their panes and streams.
   */
  list: publicProcedure.query(async () => {
    const allWindows = db.select().from(windows).orderBy(windows.createdAt).all();
    const allPanes = db.select().from(panes).all();
    const activeRow = db.select().from(uiState).where(eq3(uiState.key, "active_window_id")).get();
    const streamIds = [...new Set(
      allPanes.map((p2) => p2.eventStream?.streamId ?? null).filter((id) => id !== null)
    )];
    const streamsData = await amux.getStreams(streamIds);
    const streamMap = new Map(streamsData.map((s) => [s.id, s]));
    const streamConfigs2 = await amux.getStreamConfigs();
    return {
      windows: allWindows.map((w) => ({
        ...w,
        panes: allPanes.filter((p2) => p2.windowId === w.id).map((p2) => ({
          ...p2,
          // Enrich eventStream with full stream data
          stream: p2.eventStream ? streamMap.get(p2.eventStream.streamId) ?? null : null
        }))
      })),
      activeWindowId: activeRow?.value ?? allWindows[allWindows.length - 1]?.id ?? null,
      streamConfigs: streamConfigs2,
      // Legacy alias
      agentConfigs: streamConfigs2
    };
  }),
  /**
   * Get a single window by ID.
   */
  get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {
    const window = db.select().from(windows).where(eq3(windows.id, input.id)).get();
    if (!window) {
      throw new Error(`Window ${input.id} not found`);
    }
    const windowPanes = db.select().from(panes).where(eq3(panes.windowId, input.id)).all();
    const streamIds = windowPanes.map((p2) => p2.eventStream?.streamId ?? null).filter((id) => id !== null);
    const streamsData = await amux.getStreams(streamIds);
    const streamMap = new Map(streamsData.map((s) => [s.id, s]));
    return {
      ...window,
      panes: windowPanes.map((p2) => ({
        ...p2,
        stream: p2.eventStream ? streamMap.get(p2.eventStream.streamId) ?? null : null
      }))
    };
  }),
  /**
   * Create a new window with a single pane.
   * If streamConfigId is not provided, pane is created with no content (shows picker).
   */
  create: publicProcedure.input(z.object({
    id: z.string().optional(),
    title: z.string().optional(),
    directory: z.string().optional(),
    streamConfigId: z.string().optional(),
    // Legacy alias
    agentConfigId: z.string().optional()
  })).mutation(async ({ input }) => {
    const directory = input.directory ?? process.cwd();
    const configId = input.streamConfigId ?? input.agentConfigId;
    let stream = null;
    if (configId) {
      stream = await amux.createStream({
        directory,
        streamConfigId: configId
      });
    }
    const windowId = input.id ?? randomUUID3();
    const paneId = randomUUID3();
    const existingCount = db.select().from(windows).all().length;
    const layout = createLeaf(paneId);
    db.insert(windows).values({
      id: windowId,
      title: input.title || generateTitle(existingCount, directory),
      hasCustomTitle: !!input.title,
      layout,
      activePaneId: paneId
    }).run();
    db.insert(panes).values({
      id: paneId,
      windowId,
      content: null,
      eventStream: stream ? { type: stream.streamType, streamId: stream.id } : null
    }).run();
    db.insert(uiState).values({ key: "active_window_id", value: windowId }).onConflictDoUpdate({ target: uiState.key, set: { value: windowId } }).run();
    const newWindow = db.select().from(windows).where(eq3(windows.id, windowId)).get();
    const newPanes = db.select().from(panes).where(eq3(panes.windowId, windowId)).all();
    return {
      ...newWindow,
      panes: newPanes.map((p2) => ({
        ...p2,
        stream: stream && p2.eventStream?.streamId === stream.id ? stream : null
      }))
    };
  }),
  /**
   * Update a window (title, etc).
   */
  update: publicProcedure.input(z.object({
    id: z.string(),
    title: z.string().optional()
  })).mutation(async ({ input }) => {
    const updates = {};
    if (input.title !== void 0) {
      updates.title = input.title;
      updates.hasCustomTitle = true;
    }
    updates.lastAccessedAt = /* @__PURE__ */ new Date();
    db.update(windows).set(updates).where(eq3(windows.id, input.id)).run();
    return db.select().from(windows).where(eq3(windows.id, input.id)).get();
  }),
  /**
   * Delete a window and all its panes/streams.
   */
  delete: publicProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
    const windowPanes = db.select().from(panes).where(eq3(panes.windowId, input.id)).all();
    for (const pane of windowPanes) {
      if (pane.eventStream) {
        await amux.deleteStream(pane.eventStream.streamId);
      }
    }
    db.delete(windows).where(eq3(windows.id, input.id)).run();
    const activeRow = db.select().from(uiState).where(eq3(uiState.key, "active_window_id")).get();
    if (activeRow?.value === input.id) {
      const remaining = db.select().from(windows).orderBy(windows.createdAt).all();
      const newActiveId = remaining[remaining.length - 1]?.id ?? null;
      if (newActiveId) {
        db.update(uiState).set({ value: newActiveId }).where(eq3(uiState.key, "active_window_id")).run();
      } else {
        db.delete(uiState).where(eq3(uiState.key, "active_window_id")).run();
      }
    }
    return { ok: true };
  }),
  /**
   * Set the active window.
   */
  setActive: publicProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
    db.insert(uiState).values({ key: "active_window_id", value: input.id }).onConflictDoUpdate({ target: uiState.key, set: { value: input.id } }).run();
    db.update(windows).set({ lastAccessedAt: /* @__PURE__ */ new Date() }).where(eq3(windows.id, input.id)).run();
    return { ok: true };
  })
});

// src/trpc/layout.ts
import { z as z2 } from "zod";
import { randomUUID as randomUUID4 } from "crypto";
import * as path6 from "path";
import * as os3 from "os";
import { eq as eq4 } from "drizzle-orm";
function expandTilde(p2) {
  if (p2.startsWith("~/")) {
    return path6.join(os3.homedir(), p2.slice(2));
  }
  if (p2 === "~") {
    return os3.homedir();
  }
  return p2;
}
var amux2;
function setAmuxBridge2(bridge) {
  amux2 = bridge;
}
var layoutRouter = router({
  /**
   * Split a pane horizontally or vertically.
   * If source pane has a session, clones it. Otherwise creates empty pane.
   */
  split: publicProcedure.input(z2.object({
    windowId: z2.string(),
    paneId: z2.string(),
    direction: z2.enum(["horizontal", "vertical"]),
    // Client can provide IDs for optimistic updates
    newPaneId: z2.string().optional(),
    newSessionId: z2.string().optional()
  })).mutation(async ({ input }) => {
    const sourcePane = db.select().from(panes).where(eq4(panes.id, input.paneId)).get();
    if (!sourcePane) {
      throw new Error(`Pane ${input.paneId} not found`);
    }
    const window = db.select().from(windows).where(eq4(windows.id, input.windowId)).get();
    if (!window) {
      throw new Error(`Window ${input.windowId} not found`);
    }
    const newPaneId = input.newPaneId ?? randomUUID4();
    db.insert(panes).values({
      id: newPaneId,
      windowId: input.windowId,
      content: null,
      eventStream: null
    }).run();
    const currentLayout = window.layout;
    const newLayout = splitPane(currentLayout, input.paneId, newPaneId, input.direction);
    db.update(windows).set({
      layout: newLayout,
      activePaneId: newPaneId,
      lastAccessedAt: /* @__PURE__ */ new Date()
    }).where(eq4(windows.id, input.windowId)).run();
    const now = /* @__PURE__ */ new Date();
    return {
      newPaneId,
      newStreamId: null,
      layout: newLayout,
      newPane: {
        id: newPaneId,
        windowId: input.windowId,
        content: null,
        eventStream: null,
        createdAt: now.toISOString(),
        stream: null
      },
      // Legacy aliases
      newSessionId: null
    };
  }),
  /**
   * Close a pane. If it's the last pane, close the window.
   */
  closePane: publicProcedure.input(z2.object({
    windowId: z2.string(),
    paneId: z2.string()
  })).mutation(async ({ input }) => {
    const window = db.select().from(windows).where(eq4(windows.id, input.windowId)).get();
    if (!window) {
      throw new Error(`Window ${input.windowId} not found`);
    }
    const pane = db.select().from(panes).where(eq4(panes.id, input.paneId)).get();
    if (!pane) {
      throw new Error(`Pane ${input.paneId} not found`);
    }
    const currentLayout = window.layout;
    const allPaneIds = getAllPaneIds(currentLayout);
    if (allPaneIds.length === 1) {
      if (pane.eventStream) {
        await amux2.deleteStream(pane.eventStream.streamId);
      }
      db.delete(windows).where(eq4(windows.id, input.windowId)).run();
      return { windowClosed: true, layout: null, activePaneId: null };
    }
    if (pane.eventStream) {
      await amux2.deleteStream(pane.eventStream.streamId);
    }
    db.delete(panes).where(eq4(panes.id, input.paneId)).run();
    const newLayout = removePane(currentLayout, input.paneId);
    if (!newLayout) {
      throw new Error("Failed to remove pane from layout");
    }
    const newActivePaneId = window.activePaneId === input.paneId ? getAllPaneIds(newLayout)[0] : window.activePaneId;
    db.update(windows).set({
      layout: newLayout,
      activePaneId: newActivePaneId,
      lastAccessedAt: /* @__PURE__ */ new Date()
    }).where(eq4(windows.id, input.windowId)).run();
    return {
      windowClosed: false,
      layout: newLayout,
      activePaneId: newActivePaneId
    };
  }),
  /**
   * Set the active pane within a window.
   */
  setActivePane: publicProcedure.input(z2.object({
    windowId: z2.string(),
    paneId: z2.string()
  })).mutation(async ({ input }) => {
    db.update(windows).set({
      activePaneId: input.paneId,
      lastAccessedAt: /* @__PURE__ */ new Date()
    }).where(eq4(windows.id, input.windowId)).run();
    return { ok: true };
  }),
  /**
   * Navigate to an adjacent pane.
   */
  navigate: publicProcedure.input(z2.object({
    windowId: z2.string(),
    currentPaneId: z2.string(),
    direction: z2.enum(["left", "right", "up", "down"])
  })).mutation(async ({ input }) => {
    const window = db.select().from(windows).where(eq4(windows.id, input.windowId)).get();
    if (!window) {
      throw new Error(`Window ${input.windowId} not found`);
    }
    const currentLayout = window.layout;
    const adjacentPaneId = getAdjacentPane(currentLayout, input.currentPaneId, input.direction);
    if (adjacentPaneId) {
      db.update(windows).set({
        activePaneId: adjacentPaneId,
        lastAccessedAt: /* @__PURE__ */ new Date()
      }).where(eq4(windows.id, input.windowId)).run();
      return { newActivePaneId: adjacentPaneId };
    }
    return { newActivePaneId: null };
  }),
  /**
   * Update the split ratio at a specific position.
   */
  setRatio: publicProcedure.input(z2.object({
    windowId: z2.string(),
    path: z2.array(z2.enum(["first", "second"])),
    ratio: z2.number().min(10).max(90)
  })).mutation(async ({ input }) => {
    const window = db.select().from(windows).where(eq4(windows.id, input.windowId)).get();
    if (!window) {
      throw new Error(`Window ${input.windowId} not found`);
    }
    const currentLayout = window.layout;
    const newLayout = updateRatio(currentLayout, input.path, input.ratio);
    db.update(windows).set({ layout: newLayout }).where(eq4(windows.id, input.windowId)).run();
    return { layout: newLayout };
  }),
  /**
   * Set static content for a pane (browser URL, etc.).
   * Upsert behavior - works for both initial set and updates.
   */
  setPaneContent: publicProcedure.input(z2.object({
    paneId: z2.string(),
    content: z2.record(z2.unknown()).nullable(),
    rendererId: z2.string().optional()
  })).mutation(async ({ input }) => {
    const pane = db.select().from(panes).where(eq4(panes.id, input.paneId)).get();
    if (!pane) {
      throw new Error(`Pane ${input.paneId} not found`);
    }
    db.update(panes).set({
      content: input.content,
      rendererId: input.rendererId ?? null
    }).where(eq4(panes.id, input.paneId)).run();
    return { content: input.content, rendererId: input.rendererId };
  }),
  /**
   * Set an event stream for a pane (agent or shell).
   * Creates stream in amux and starts the backend.
   */
  setPaneEventStream: publicProcedure.input(z2.object({
    paneId: z2.string(),
    streamConfigId: z2.string(),
    directory: z2.string().optional(),
    rendererId: z2.string().optional(),
    // Legacy alias
    agentConfigId: z2.string().optional()
  })).mutation(async ({ input }) => {
    const pane = db.select().from(panes).where(eq4(panes.id, input.paneId)).get();
    if (!pane) {
      throw new Error(`Pane ${input.paneId} not found`);
    }
    if (pane.eventStream) {
      throw new Error(`Pane ${input.paneId} already has an event stream`);
    }
    const configId = input.streamConfigId ?? input.agentConfigId;
    const directory = input.directory ? expandTilde(input.directory) : process.cwd();
    const stream = await amux2.createStream({
      directory,
      streamConfigId: configId
    });
    const eventStream = { type: stream.streamType, streamId: stream.id };
    db.update(panes).set({
      eventStream,
      rendererId: input.rendererId ?? null
    }).where(eq4(panes.id, input.paneId)).run();
    await amux2.startStream(stream.id);
    return {
      streamId: stream.id,
      eventStream,
      stream: {
        id: stream.id,
        directory: stream.directory,
        streamConfigId: stream.streamConfigId,
        streamType: stream.streamType,
        model: stream.model,
        mode: stream.mode,
        createdAt: stream.createdAt.toISOString()
      },
      // Legacy aliases
      sessionId: stream.id,
      process: eventStream,
      session: {
        id: stream.id,
        directory: stream.directory,
        agentConfigId: stream.streamConfigId,
        model: stream.model,
        mode: stream.mode,
        createdAt: stream.createdAt.toISOString()
      }
    };
  })
});

// src/trpc/agents.ts
import { z as z3 } from "zod";
import { observable } from "@trpc/server/observable";

// src/lib/panes.ts
import { eq as eq5 } from "drizzle-orm";
function getStreamIdForPane(paneId) {
  const pane = db.select().from(panes).where(eq5(panes.id, paneId)).get();
  if (!pane) {
    throw new Error(`Pane ${paneId} not found`);
  }
  if (!pane.eventStream) {
    throw new Error(`Pane ${paneId} does not have an event stream`);
  }
  return pane.eventStream.streamId;
}

// src/trpc/agents.ts
var amux3;
function setAmuxBridge3(bridge) {
  amux3 = bridge;
}
var paneInput = z3.object({
  paneId: z3.string()
});
var streamsRouter = router({
  /**
   * Start stream for a pane.
   */
  start: publicProcedure.input(paneInput).mutation(async ({ input }) => {
    const streamId = getStreamIdForPane(input.paneId);
    return amux3.startStream(streamId);
  }),
  /**
   * Stop stream for a pane.
   */
  stop: publicProcedure.input(paneInput).mutation(async ({ input }) => {
    const streamId = getStreamIdForPane(input.paneId);
    await amux3.stopStream(streamId);
    return { ok: true };
  }),
  /**
   * Send input to a pane's stream.
   */
  input: publicProcedure.input(z3.object({
    paneId: z3.string(),
    message: z3.string()
  })).mutation(async ({ input }) => {
    const streamId = getStreamIdForPane(input.paneId);
    await amux3.input(streamId, input.message);
    return { ok: true };
  }),
  /**
   * Cancel the current prompt.
   */
  cancel: publicProcedure.input(paneInput).mutation(async ({ input }) => {
    const streamId = getStreamIdForPane(input.paneId);
    await amux3.cancel(streamId);
    return { ok: true };
  }),
  /**
   * Set stream mode.
   */
  setMode: publicProcedure.input(z3.object({
    paneId: z3.string(),
    modeId: z3.string()
  })).mutation(async ({ input }) => {
    const streamId = getStreamIdForPane(input.paneId);
    await amux3.setMode(streamId, input.modeId);
    return { ok: true };
  }),
  /**
   * Set stream model.
   */
  setModel: publicProcedure.input(z3.object({
    paneId: z3.string(),
    modelId: z3.string()
  })).mutation(async ({ input }) => {
    const streamId = getStreamIdForPane(input.paneId);
    await amux3.setModel(streamId, input.modelId);
    return { ok: true };
  }),
  /**
   * Respond to a permission request.
   */
  respondPermission: publicProcedure.input(z3.object({
    paneId: z3.string(),
    requestId: z3.string(),
    optionId: z3.string()
  })).mutation(async ({ input }) => {
    const streamId = getStreamIdForPane(input.paneId);
    amux3.respondPermission(streamId, input.requestId, input.optionId);
    return { ok: true };
  }),
  /**
   * Subscribe to updates for a pane's stream.
   */
  subscribe: publicProcedure.input(paneInput).subscription(({ input }) => {
    const streamId = getStreamIdForPane(input.paneId);
    return observable((emit) => {
      return amux3.subscribeToStream(streamId, (update) => {
        emit.next(update);
      });
    });
  })
});

// src/trpc/files.ts
import { z as z4 } from "zod";
import * as fs6 from "fs/promises";
import * as path7 from "path";
import * as os4 from "os";
function expandTilde2(p2) {
  if (p2.startsWith("~/")) {
    return path7.join(os4.homedir(), p2.slice(2));
  }
  if (p2 === "~") {
    return os4.homedir();
  }
  return p2;
}
var amux4;
function setAmuxBridge4(bridge) {
  amux4 = bridge;
}
var filesRouter = router({
  /**
   * Get server's current working directory.
   */
  getCwd: publicProcedure.query(() => process.cwd()),
  /**
   * List directories in a given path.
   * Used by DirectoryPicker for browsing.
   */
  listDirectory: publicProcedure.input(z4.object({
    directory: z4.string()
  })).query(async ({ input }) => {
    try {
      const absPath = path7.resolve(expandTilde2(input.directory));
      const entries = await fs6.readdir(absPath, { withFileTypes: true });
      return entries.filter((e) => e.isDirectory()).map((e) => ({ name: e.name, isDirectory: true })).sort((a, b) => a.name.localeCompare(b.name));
    } catch {
      return [];
    }
  }),
  /**
   * List files/directories matching a partial path for autocomplete.
   * Returns files in the session's working directory.
   */
  listForAutocomplete: publicProcedure.input(z4.object({
    paneId: z4.string(),
    partialPath: z4.string(),
    limit: z4.number().default(20)
  })).query(async ({ input }) => {
    const streamId = getStreamIdForPane(input.paneId);
    return amux4.listFilesForAutocomplete(streamId, input.partialPath, input.limit);
  })
});

// src/trpc/terminals.ts
import { z as z5 } from "zod";
import { eq as eq6 } from "drizzle-orm";
var amux5;
function setAmuxBridge5(bridge) {
  amux5 = bridge;
}
var paneInput2 = z5.object({
  paneId: z5.string()
});
function getStreamIdForPane2(paneId) {
  const pane = db.select().from(panes).where(eq6(panes.id, paneId)).get();
  if (!pane) {
    throw new Error(`Pane ${paneId} not found`);
  }
  if (!pane.eventStream) {
    throw new Error(`Pane ${paneId} does not have an event stream`);
  }
  return pane.eventStream.streamId;
}
var terminalsRouter = router({
  /**
   * Write raw input to terminal (keystrokes from client).
   */
  write: publicProcedure.input(
    z5.object({
      paneId: z5.string(),
      data: z5.string()
    })
  ).mutation(({ input }) => {
    const streamId = getStreamIdForPane2(input.paneId);
    amux5.terminalWrite(streamId, input.data);
    return { ok: true };
  }),
  /**
   * Resize terminal dimensions.
   */
  resize: publicProcedure.input(
    z5.object({
      paneId: z5.string(),
      cols: z5.number().int().positive(),
      rows: z5.number().int().positive()
    })
  ).mutation(({ input }) => {
    const streamId = getStreamIdForPane2(input.paneId);
    amux5.terminalResize(streamId, input.cols, input.rows);
    return { ok: true };
  }),
  /**
   * Check if a pane is a terminal stream.
   */
  isTerminal: publicProcedure.input(paneInput2).query(({ input }) => {
    const streamId = getStreamIdForPane2(input.paneId);
    return { isTerminal: amux5.isTerminalStream(streamId) };
  }),
  /**
   * Set terminal title (from OSC escape sequence).
   */
  setTitle: publicProcedure.input(z5.object({
    paneId: z5.string(),
    title: z5.string()
  })).mutation(({ input }) => {
    const streamId = getStreamIdForPane2(input.paneId);
    amux5.updateStreamTitle(streamId, input.title);
    return { ok: true };
  })
});

// src/trpc/renderers.ts
import { observable as observable2 } from "@trpc/server/observable";

// src/lib/rendererWatcher.ts
import chokidar2 from "chokidar";
import * as path9 from "path";
import * as os5 from "os";
import * as fs8 from "fs/promises";
import { EventEmitter as EventEmitter2 } from "events";

// src/lib/rendererCompiler.ts
import { build as build2 } from "esbuild";
import * as fs7 from "fs/promises";
import * as path8 from "path";
async function compileRenderer(pluginDir) {
  const metaPath = path8.join(pluginDir, "meta.json");
  const entryPath = path8.join(pluginDir, "index.tsx");
  const pkgPath = path8.join(pluginDir, "package.json");
  let meta;
  try {
    const metaContent = await fs7.readFile(metaPath, "utf-8");
    meta = JSON.parse(metaContent);
  } catch (err) {
    throw new Error(
      `Failed to read meta file for ${pluginDir}. Expected ${metaPath} with { name, target } properties. Error: ${err instanceof Error ? err.message : String(err)}`
    );
  }
  if (!meta.name || typeof meta.name !== "string") {
    throw new Error(`Meta file ${metaPath} must have a 'name' string property`);
  }
  if (!meta.target || !meta.target.kind) {
    throw new Error(`Meta file ${metaPath} must have a 'target' object with 'kind' property`);
  }
  let hasPackageJson = false;
  try {
    const pkgContent = await fs7.readFile(pkgPath, "utf-8");
    const pkg = JSON.parse(pkgContent);
    hasPackageJson = true;
    const deps = pkg.dependencies || {};
    if (deps["react"] || deps["react-dom"]) {
      throw new Error(
        `Plugin "${meta.name}": react and react-dom must be in peerDependencies, not dependencies. This ensures plugins share the same React instance as the host app.`
      );
    }
  } catch (err) {
    if (err instanceof Error && err.message.includes("peerDependencies")) {
      throw err;
    }
  }
  const result = await build2({
    entryPoints: [entryPath],
    bundle: true,
    write: false,
    format: "esm",
    jsx: "automatic",
    target: "es2020",
    external: ["react", "react-dom", "react/jsx-runtime", "@bytespell/shella-sdk", "@bytespell/shella-sdk/components/ui"],
    // If plugin has package.json, look for node_modules
    nodePaths: hasPackageJson ? [path8.join(pluginDir, "node_modules")] : [],
    minify: false
  });
  const code = result.outputFiles[0].text;
  const id = `user:${path8.basename(pluginDir)}`;
  return {
    id,
    name: meta.name,
    description: meta.description,
    target: meta.target,
    code
  };
}
async function isValidPlugin(dirPath) {
  try {
    await fs7.access(path8.join(dirPath, "meta.json"));
    await fs7.access(path8.join(dirPath, "index.tsx"));
    return true;
  } catch {
    return false;
  }
}

// src/lib/rendererWatcher.ts
var RendererEventEmitter = class extends EventEmitter2 {
  emit(event, data) {
    return super.emit(event, data);
  }
  on(event, listener) {
    return super.on(event, listener);
  }
  off(event, listener) {
    return super.off(event, listener);
  }
};
function watchRenderers(emitter) {
  const userPath = path9.join(os5.homedir(), ".shella", "renderers");
  ensureDir2(userPath);
  const knownPlugins = /* @__PURE__ */ new Map();
  const pendingCompiles = /* @__PURE__ */ new Map();
  const watcher = chokidar2.watch(userPath, {
    ignoreInitial: false,
    persistent: true,
    awaitWriteFinish: {
      stabilityThreshold: 100,
      pollInterval: 50
    },
    depth: 3
    // Watch inside plugin folders for nested components
  });
  const getPluginDir = (filePath) => {
    const relative3 = path9.relative(userPath, filePath);
    const parts = relative3.split(path9.sep);
    if (parts.length >= 1 && parts[0]) {
      return path9.join(userPath, parts[0]);
    }
    return null;
  };
  const scheduleCompile = (pluginDir) => {
    if (pendingCompiles.has(pluginDir)) {
      clearTimeout(pendingCompiles.get(pluginDir));
    }
    pendingCompiles.set(
      pluginDir,
      setTimeout(async () => {
        pendingCompiles.delete(pluginDir);
        await handlePluginChange(pluginDir);
      }, 100)
    );
  };
  const handlePluginChange = async (pluginDir) => {
    if (!await isValidPlugin(pluginDir)) return;
    try {
      const compiled = await compileRenderer(pluginDir);
      if (!knownPlugins.has(pluginDir)) {
        knownPlugins.set(pluginDir, true);
        console.log(`[renderers] Added: ${compiled.id}`);
        emitter.emit("renderer:added", compiled);
      } else {
        console.log(`[renderers] Updated: ${compiled.id}`);
        emitter.emit("renderer:updated", { id: compiled.id, code: compiled.code });
      }
    } catch (err) {
      console.error(`[renderers] Error compiling ${pluginDir}:`, err);
      emitter.emit("renderer:error", {
        filePath: pluginDir,
        error: err instanceof Error ? err : new Error(String(err))
      });
    }
  };
  watcher.on("add", (filePath) => {
    const pluginDir = getPluginDir(filePath);
    if (pluginDir) scheduleCompile(pluginDir);
  });
  watcher.on("change", (filePath) => {
    const pluginDir = getPluginDir(filePath);
    if (pluginDir) scheduleCompile(pluginDir);
  });
  watcher.on("unlinkDir", (dirPath) => {
    if (knownPlugins.has(dirPath)) {
      const id = `user:${path9.basename(dirPath)}`;
      knownPlugins.delete(dirPath);
      console.log(`[renderers] Removed: ${id}`);
      emitter.emit("renderer:removed", { id });
    }
  });
  watcher.on("error", (err) => {
    console.error("[renderers] Watcher error:", err);
  });
  return () => {
    for (const timeout of pendingCompiles.values()) {
      clearTimeout(timeout);
    }
    pendingCompiles.clear();
    watcher.close();
  };
}
async function ensureDir2(dirPath) {
  try {
    await fs8.mkdir(dirPath, { recursive: true });
  } catch {
  }
}
async function scanRenderers() {
  const userPath = path9.join(os5.homedir(), ".shella", "renderers");
  const renderers = [];
  try {
    const entries = await fs8.readdir(userPath, { withFileTypes: true });
    for (const entry of entries) {
      if (!entry.isDirectory()) continue;
      const pluginDir = path9.join(userPath, entry.name);
      if (await isValidPlugin(pluginDir)) {
        try {
          const compiled = await compileRenderer(pluginDir);
          renderers.push(compiled);
        } catch (err) {
          console.error(`[renderers] Error compiling ${pluginDir}:`, err);
        }
      }
    }
  } catch {
  }
  return renderers;
}

// src/trpc/renderers.ts
var rendererEmitter = new RendererEventEmitter();
var stopWatcher = null;
function initRendererWatcher() {
  if (stopWatcher) {
    stopWatcher();
  }
  stopWatcher = watchRenderers(rendererEmitter);
}
var renderersRouter = router({
  /**
   * List all currently available user renderers
   * Used for initial hydration when client connects.
   */
  list: publicProcedure.query(async () => {
    return scanRenderers();
  }),
  /**
   * Subscribe to renderer additions
   */
  onAdded: publicProcedure.subscription(() => {
    return observable2((emit) => {
      const handler = (data) => emit.next(data);
      rendererEmitter.on("renderer:added", handler);
      return () => rendererEmitter.off("renderer:added", handler);
    });
  }),
  /**
   * Subscribe to renderer updates (code changes)
   */
  onUpdated: publicProcedure.subscription(() => {
    return observable2((emit) => {
      const handler = (data) => emit.next(data);
      rendererEmitter.on("renderer:updated", handler);
      return () => rendererEmitter.off("renderer:updated", handler);
    });
  }),
  /**
   * Subscribe to renderer removals
   */
  onRemoved: publicProcedure.subscription(() => {
    return observable2((emit) => {
      const handler = (data) => emit.next(data);
      rendererEmitter.on("renderer:removed", handler);
      return () => rendererEmitter.off("renderer:removed", handler);
    });
  })
});

// src/trpc/router.ts
function initializeRouters(amux6) {
  setAmuxBridge(amux6);
  setAmuxBridge2(amux6);
  setAmuxBridge3(amux6);
  setAmuxBridge4(amux6);
  setAmuxBridge5(amux6);
  initRendererWatcher();
}
var shellaRouter = router({
  windows: windowsRouter,
  layout: layoutRouter,
  streams: streamsRouter,
  files: filesRouter,
  terminals: terminalsRouter,
  renderers: renderersRouter,
  // Legacy alias
  agents: streamsRouter
});

// src/index.ts
discoverAndRegisterConfigs(builtInDiscoverers);
var __dirname2 = path10.dirname(fileURLToPath2(import.meta.url));
function createShellaServer(options = {}) {
  const port = options.port ?? 3067;
  if (options.verbose) {
    setVerbose(true);
  }
  const driverEmitter = new DriverEventEmitter();
  let stopDriverWatcher = null;
  driverEmitter.on("driver:added", ({ id, name }) => {
    debug("shella", `Driver registered: ${id} (${name})`);
  });
  driverEmitter.on("driver:updated", ({ id, name }) => {
    debug("shella", `Driver updated: ${id} (${name})`);
  });
  driverEmitter.on("driver:removed", ({ id }) => {
    debug("shella", `Driver removed: ${id}`);
  });
  driverEmitter.on("driver:error", ({ pluginDir, error }) => {
    console.error(`Driver error in ${pluginDir}:`, error.message);
  });
  const amuxBridge = createAmuxBridge();
  initializeRouters(amuxBridge);
  const app = express();
  app.use(express.json());
  const publicPath = path10.join(__dirname2, "..", "public");
  app.use(express.static(publicPath));
  app.use("/trpc", createExpressMiddleware({ router: shellaRouter }));
  app.get("/health", (_req, res) => res.json({ status: "ok" }));
  app.get("/{*splat}", (_req, res) => {
    res.sendFile(path10.join(publicPath, "index.html"));
  });
  const server = createServer(app);
  const wss = new WebSocketServer({ server, path: "/trpc" });
  applyWSSHandler({ wss, router: shellaRouter });
  return {
    start: async () => {
      await scanDrivers();
      stopDriverWatcher = watchDrivers(driverEmitter);
      return new Promise((resolve4, reject) => {
        server.once("error", (err) => {
          if (err.code === "EADDRINUSE") {
            reject(new Error(`Port ${port} is already in use. Kill the other process or use a different port.`));
          } else {
            reject(err);
          }
        });
        server.listen(port, () => {
          const url = `http://localhost:${port}`;
          debug("shella", `Running on ${url}`);
          options.onReady?.(url);
          resolve4();
        });
      });
    },
    stop: async () => {
      debug("shella", "Shutting down...");
      stopDriverWatcher?.();
      wss.close();
      await muxManager3.stopAll();
      server.close();
    }
  };
}

// bin/cli.ts
var VERSION = "0.1.0";
var BRAND = "\x1B[38;2;99;102;241m";
var RESET = "\x1B[0m";
var DIM = "\x1B[2m";
var brand = (text2) => `${BRAND}${text2}${RESET}`;
var dim = (text2) => `${DIM}${text2}${RESET}`;
function getLanIp() {
  const interfaces = networkInterfaces();
  for (const [name, addrs] of Object.entries(interfaces)) {
    if (/^(lo|docker|veth|br-|virbr)/.test(name)) continue;
    for (const addr of addrs || []) {
      if (addr.family === "IPv4" && !addr.internal) {
        return addr.address;
      }
    }
  }
  return "localhost";
}
async function startCommand(options) {
  const port = parseInt(options.port, 10);
  const verbose = options.verbose;
  const s = verbose ? null : p.spinner();
  const log2 = (msg) => verbose && console.log(msg);
  if (!verbose) {
    p.intro(`${brand("shella")} ${dim(`v${VERSION}`)}`);
  } else {
    log2(`${brand("shella")} ${dim(`v${VERSION}`)}`);
  }
  if (s) s.start("Starting server...");
  else log2("Starting server...");
  const shella = createShellaServer({
    port,
    verbose,
    onReady: (url) => {
      if (s) s.stop("Ready");
      const lanIp = getLanIp();
      const lanUrl = `http://${lanIp}:${port}`;
      if (verbose) {
        log2(`
${brand(lanUrl)}`);
        log2(dim("access from any device"));
        log2(`
${dim("press ctrl+c to stop")}
`);
      } else {
        p.log.success(`${brand(lanUrl)}`);
        p.log.message(dim("access from any device"));
        p.outro(dim("press ctrl+c to stop"));
      }
      if (options.open) {
        const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
        spawn(openCommand, [url], { stdio: "ignore", detached: true }).unref();
      }
    }
  });
  try {
    await shella.start();
  } catch (err) {
    if (s) s.stop("Failed");
    p.log.error(err instanceof Error ? err.message : String(err));
    process.exit(1);
  }
  const shutdown = async () => {
    console.log("");
    console.log(dim("stopping..."));
    await shella.stop();
    console.log(`${brand("shella")} ${dim("stopped")}`);
    process.exit(0);
  };
  process.on("SIGTERM", shutdown);
  process.on("SIGINT", shutdown);
}
program.name("shella").description(`${brand("shella")} - Self-hosted AI coding agents. Access from your phone.`).version(VERSION).option("-p, --port <port>", "Server port", "3067").option("--no-open", "Don't open browser automatically").option("-v, --verbose", "Show detailed logs", false).action(startCommand);
program.parse();
//# sourceMappingURL=cli.js.map