Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134import type { Files } from "../src/files.ts";import type { Route } from "../src/pages/route.ts";import { platformCommands, runRequestedCommands, type Ask, type ImportRepo } from "../src/platform/commands.ts";import { openHref, openKey, type OpenProject, type Platform } from "../src/platform/state.ts";import { commands } from "../src/shell/commands.ts";import { bindKeys } from "../src/shell/dom/keys.ts";import "../src/shell/dom/shell.ts";import { mainStack, parseLayout, Workbench, type Unsubscribe } from "../src/shell/index.ts";import { openListeningPorts } from "../src/auto-browser.ts";import { needsInstall, runInTerminal } from "../src/run-in-terminal.ts";import { connectTo, sandbox } from "../src/sandbox.ts";import { draftSandbox, projectSandbox } from "../src/sandbox-name.ts";import { stockViews } from "../src/views/index.ts";
/** The editor: the shell, its workbench, views, and keys. main.ts imports it only for `/new`, `…/edit`, and a tangled repo's address. */export interface EditorContext { readonly platform: Platform; readonly header: HTMLElementTagNameMap["nib-app-header"]; readonly ask: Ask; readonly importRepo: ImportRepo; /** The files shown before any project opens. */ readonly demo: Files; readonly route: Route; /** Settles once the address's project or repo has opened, or failed to. */ readonly opened: Promise<unknown>;}
/** The sandbox for what is open: a saved project's own, anything else the tab's. */const sandboxFor = (open: OpenProject | undefined) => (open?.kind === "project" ? projectSandbox(open.project.uri) : Promise.resolve(draftSandbox())).then(connectTo);
const STORAGE_KEY = "nib.shell.layout";
export function startEditor({ platform, header, ask, importRepo, demo, route, opened }: EditorContext): void { const layout = new URLSearchParams(location.search).has("reset") ? undefined : stored(); const workbench = new Workbench(layout ? { layout } : {});
if (!layout) { workbench.open({ kind: "explorer", input: { root: "/" } }); workbench.open({ kind: "editor", input: { path: "index.ts" } }); workbench.open({ kind: "browser", input: { url: null } }, { beside: mainStack(workbench.layout.root).id, edge: "right" }); workbench.open({ kind: "processes", input: { sandbox: "default" } }); workbench.open({ kind: "terminal", input: { cwd: "/", session: "default" } }); workbench.open({ kind: "agent", input: { conversation: "default" } }); }
workbench.subscribe((next) => { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); } catch { // Storage is a convenience here; the shell works without it. } });
const shell = document.createElement("nib-shell"); shell.id = "shell"; shell.floatingPage = "/float.html"; // ?compact or ?desktop forces a presentation, so the phone layout can be tried with a mouse. const params = new URLSearchParams(location.search); if (params.has("compact")) shell.mode = "compact"; if (params.has("desktop")) shell.mode = "desktop";
document.body.append(shell); if (params.has("dragtrace")) void import("./drag-trace.ts").then(({ traceDrags }) => traceDrags(shell));
// A sandbox lives as long as the files it runs: saving or forking keeps them, and opening another // project replaces them. The first waits for the address's project to open, so the demo shown while it // loads shares that project's sandbox instead of booting one of its own. const first = route.kind === "project" ? opened.then(() => platform.state.project) : Promise.resolve(undefined); let box = sandbox(() => first.then(sandboxFor)); let offPorts = openListeningPorts(box, workbench); let fresh = true; let shown: Files | undefined; let disconnect: Unsubscribe = () => {}; const showFiles = () => { const open = platform.state.project; const files = open?.files ?? demo; if (files === shown) return; if (shown && !(fresh && shown === demo)) { offPorts(); box.close(); box = sandbox(() => sandboxFor(open)); offPorts = openListeningPorts(box, workbench); } if (shown) fresh = false; shown = files; box.mirror(files); disconnect(); disconnect = shell.connect(workbench, stockViews(files, box)); // The kernel starts empty on every load, so a project with dependencies installs them where // its output shows, as a fresh clone's first step would. if (needsInstall(files)) runInTerminal(box, workbench, "npm install"); }; showFiles(); platform.subscribe(showFiles);
const table = [...commands, ...platformCommands(platform, ask, importRepo)]; const bound = table.map((c) => ({ id: c.id, run: () => c.run(workbench) })); header.editor = true; header.connect(platform, bound); runRequestedCommands(bound); bindKeys(window, workbench, table); // The address names the open project, so a reload or a shared link reopens it. Opening another // project is a navigation Back can undo; a new slug or a canonical name for the same one is not. let addressed: string | undefined; // On /…/edit the first project to open is the one the address already names. let opening = route.kind === "project" || route.kind === "repo"; platform.subscribe((state) => { const open = state.project; if (!open) return; const href = openHref(open); const navigated = !opening && addressed !== openKey(open); // A tangled blob URL names a file, not a project root; focus it once, the first time this repo opens. if (opening && route.kind === "repo" && route.file && route.path) workbench.open({ kind: "editor", input: { path: route.path } }); opening = false; addressed = openKey(open); if (location.pathname === href) return; if (navigated) history.pushState(null, "", href); else history.replaceState(null, "", href); }); // The editor holds one project, so moving through history reloads whatever the address names. addEventListener("popstate", () => location.reload()); Object.assign(globalThis, { workbench });}
function stored() { try { return parseLayout(JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "null")); } catch { return undefined; }}