{"version":3,"sources":["../src/editor.ts"],"sourcesContent":["import { protocolRequest } from './sandboxUtils';\nimport { throwOnRefusal, type CodedRefusalError } from './protocolRefusal';\nimport { SCHEMES } from './protocolSchemes';\nimport { PROTOCOL_EDITOR } from './generated/protocol';\n\n/**\n * Open a working-tree file in the immediately.run host editor (UI_AS_APPS_SPEC §4 —\n * the file explorer's click-to-open). This is an INTENT: the app asks, the HOST\n * validates the path and drives the CodeMirror editor — the editor itself stays\n * host-owned (§2 recursion boundary), so an app can never own or script it beyond\n * \"please show this file\".\n *\n * Requires the elevated `editor:open` capability — a previewed app does not hold it\n * (it must not move the host's focus), so only a system app whose binding grants it\n * (the file explorer) can call this; anyone else is refused at the gate.\n */\n\n/** An error from {@link openInEditor}, carrying a machine-readable `.code`.\n *\n *  **What you actually catch.** The SDK throws a plain `Error` with `.code` assigned — a\n *  `CodedRefusalError` from `protocolRefusal.ts` — never a distinct class. This interface\n *  documents the `.code` VALUES the host sends. Nothing enforces the union at runtime —\n *  `.code` is whatever string arrived — so treat an unlisted code as possible and\n *  `instanceof Error` as the only reliable test.\n */\nexport interface EditorOpenError extends Error {\n  code:\n    | 'forbidden' // the frame lacks `editor:open` (or `editor:reveal`, for a `reveal`)\n    | 'not-found' // no such file in the live working tree (the host never creates)\n    | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n    | 'no-target' // there is no host editor session to open files in\n    | 'unknown';\n}\n\ntype EditorResult =\n  | { ok: true; data: unknown }\n  // R3-853: a `too-large` refusal may carry the host's `limitBytes`.\n  | { ok: false; code: string; message: string; limitBytes?: number };\n\nconst editorRequest = async (method: string, arg: Record<string, unknown>): Promise<void> => {\n  const res = (await protocolRequest(SCHEMES[PROTOCOL_EDITOR], method, [arg])) as EditorResult;\n  try {\n    throwOnRefusal(res, `editor ${method} failed`);\n  } catch (e) {\n    // R3-853: a `too-large` refusal may carry the host's `limitBytes` — re-attach\n    // it to the thrown error (throwOnRefusal builds a code+message error only).\n    // Only ever set by `upload` today; forwarded generically and harmlessly. The\n    // `res &&` narrowing guard is load-bearing (review round 1): a null/undefined\n    // reply is a coded refusal, never a TypeError from this read (R3-817's property).\n    const limit = res && res.ok === false ? res.limitBytes : undefined;\n    if (typeof limit === 'number') (e as EditorWriteError).limitBytes = limit;\n    throw e;\n  }\n};\n\n/** Where in a file to land when opening it (R3-388). 1-indexed `line`, matching every\n *  diagnostic producer that feeds it (`tsc`, `eslint`, `BuildError`) and both VS Code\n *  and IntelliJ. A `line` past end-of-file CLAMPS to the last line rather than\n *  erroring — a diagnostic outlives the edit that shortened the file, and landing\n *  close beats refusing to navigate. */\nexport interface EditorSelection {\n  line: number;\n  column?: number;\n}\n\n/** Options for {@link openInEditor} (R3-389). */\nexport interface EditorOpenOptions {\n  /** Also bring the user to the editor, ACROSS activities (TOOLS_ACTIVITY_SPEC §5.2).\n   *  An app that owns the main pane (the Tools activity's runner sits where the editor\n   *  would) cannot rely on the file simply becoming visible — the editor is not on\n   *  screen — so this asks the host to switch to the activity that owns it.\n   *\n   *  This is the elevated `editor:reveal` capability, not `editor:open`: a frame\n   *  without it is refused `forbidden` for the whole call (the file is NOT opened —\n   *  never silently opened-without-moving). The host decides whether the user actually\n   *  moves: it needs a real user gesture (a click in your frame within the last few\n   *  seconds counts; a call on a timer or on run completion does not) and is\n   *  rate-limited. The promise resolves the same either way, so treat a resolved\n   *  reveal as \"asked\", not \"moved\", and keep a visible fallback control. Where the\n   *  host owns the editor activity is host state; nothing here can name it. */\n  reveal?: boolean;\n}\n\n/**\n * Ask the host to open `path` (a repo-relative working-tree path, e.g. `src/App.tsx`\n * or `/src/App.tsx`) in the editor. Resolves once the editor switches to it; rejects\n * with an {@link EditorOpenError} (`.code`) if the path is invalid, missing, or this\n * app may not open files.\n *\n * Pass `selection` to land the caret on a specific line — what a problems list needs\n * to make a diagnostic clickable. It widens nothing: a selection says where to look\n * inside a file the caller could already open, and the capability is unchanged\n * (`editor:open`).\n *\n * Pass `{ reveal: true }` to ALSO bring the user to the editor across activities —\n * see {@link EditorOpenOptions.reveal}; that one does need the elevated\n * `editor:reveal`, and is refused outright without it.\n *\n * Older hosts ignore `selection` and open the file at its existing position, so a\n * caller may pass it unconditionally. `reveal` is only sent when true, so a host that\n * predates it sees a plain open.\n */\nexport const openInEditor = (path: string, selection?: EditorSelection, opts?: EditorOpenOptions): Promise<void> =>\n  editorRequest('open', {\n    path,\n    ...(selection ? { selection } : {}),\n    ...(opts?.reveal === true ? { reveal: true } : {}),\n  });\n\n/**\n * Where to land when entering the edit experience (EDITOR_FIRST_EDITING_SPEC §6).\n *\n * Three target classes, and **at most one** may be given — supplying more than one is\n * refused `invalid-params` (client-side, before anything is sent). Omit all three to\n * edit the current route's entry.\n *\n * - **Own-source** (`path`): a repo-relative path in the CURRENT repo. Self-scoped —\n *   the app you are already running; the host navigates within the current route,\n *   never to another repo.\n * - **Mount-file** (`file`): a file in a mount you ALREADY HOLD, opened in the main\n *   edit experience (§9, settled 2026-08-28). You can only name a mount you hold: an\n *   unheld one is `forbidden`, and indistinguishably so from one that does not exist,\n *   because an app must not be able to probe for mounts (no existence oracle).\n *   Editing a file *outside* your mounts stays picker-mediated (`pick-file`).\n * - **Bundle-file** (`bundleFile`, R3-876 / APP_CUSTOMIZATION_SPEC §5a): a\n *   bundle-relative path inside a directory delegated to you READ-ONLY (an opener's\n *   chroot). The host finds which of your held delegations contains it — you never\n *   name a mount — and opens the delegation's SOURCE file in the main-pane editor\n *   under the READER's authority: your chroot is never upgraded, and no delegation\n *   is minted for you. The request needs a real user gesture (transient activation,\n *   sampled on the host document) and is refused `no-activation` without one; a\n *   path outside every delegation you hold is `forbidden`, indistinguishable from a\n *   missing one.\n *\n * A URI, a `..` segment, or a NUL is refused `invalid-params` in any class.\n */\nexport interface EditTarget {\n  /** A repo-relative working-tree path in the current repo to focus once in edit\n   *  mode (e.g. `src/App.tsx`). Omit to edit the current route's entry. */\n  path?: string;\n  /** A file in one of YOUR OWN mounts, by the portable mount reference. `relPath` is\n   *  mount-relative and leading-slash (e.g. `/notes/idea.mdx`). Mutually exclusive\n   *  with {@link EditTarget.path} and {@link EditTarget.bundleFile}.\n   *\n   *  There is no `mode` here on purpose: writability is the HOST's live reading of\n   *  the mount, not the caller's claim. A `ro` mount — which is also how an anonymous\n   *  share-link viewer surfaces — is refused `read-only` at call time, before the\n   *  editor is entered, so you never land in an editor that cannot save. */\n  file?: { mountId: string; relPath: string };\n  /** A bundle-relative, leading-slash path inside a read-only delegation you hold\n   *  (R3-876 — see the class note above). Mutually exclusive with\n   *  {@link EditTarget.path} and {@link EditTarget.file}. */\n  bundleFile?: string;\n}\n\n/** An error from {@link requestEdit}, carrying a machine-readable `.code`.\n *\n *  **What you actually catch.** The SDK throws a plain `Error` with `.code` assigned — a\n *  `CodedRefusalError` from `protocolRefusal.ts` — never a distinct class. This interface\n *  documents the `.code` VALUES the host sends. Nothing enforces the union at runtime —\n *  `.code` is whatever string arrived — so treat an unlisted code as possible and\n *  `instanceof Error` as the only reliable test.\n */\nexport interface RequestEditError extends Error {\n  code:\n    | 'read-only' // editing isn't possible here (a `ro` mount / anonymous viewer; for `bundleFile`: the READER cannot edit the source) — HIDE the affordance\n    | 'forbidden' // the host refuses: a cross-repo target, a mount you do not hold, or a `bundleFile` outside every delegation you hold\n    | 'invalid-params' // the target was malformed (URI / `..` / NUL / more than one target class at once)\n    | 'not-found' // the mount-file/bundle-file target does not exist — and asking did NOT create it\n    | 'no-target' // there is no host editor session to enter\n    | 'no-activation' // R3-876: a `bundleFile` request without transient user activation on the host document (G-CUST-11) — re-issue from a real click\n    | 'cancelled' // the user dismissed an interactive host UX along the way\n    | 'unknown';\n}\n\n/**\n * Ask the host to enter the **edit experience** for the app you are running —\n * the present→edit transition (`/present/...` → `/edit/...`) an app cannot make\n * itself. This is an INTENT (§2 recursion boundary): the app asks, the HOST\n * performs the visible, user-observable navigation and draws all editor chrome;\n * the app never navigates or paints chrome.\n *\n * Use it to offer an \"edit this\" affordance from a run/present-mode app that opens\n * the app's own source in the platform editor — instead of shipping a bespoke\n * in-app editor (EDITOR_FIRST_EDITING_SPEC §1).\n *\n * Also opens a file from a mount you already hold, in the main edit experience:\n *\n *   await requestEdit({ file: { mountId: 'space:abc', relPath: '/notes/idea.mdx' } });\n *\n * Resolves once the host begins the transition; rejects with a\n * {@link RequestEditError} (`.code`). Treat `read-only`/`forbidden` as \"editing is\n * not available — hide the affordance,\" never as an error to surface to the user.\n * `not-found` means the file is not there; nothing is created by asking to edit it.\n *\n * Naming more than one target class is refused client-side (`invalid-params`),\n * before anything reaches the wire: a precedence rule is a silent reinterpretation\n * of an ambiguous request, and the one thing worse than refusing an edit target is\n * editing a different file than the caller named.\n */\nexport const requestEdit = (target?: EditTarget): Promise<void> => {\n  const classes = [target?.path, target?.file, target?.bundleFile].filter((t) => t !== undefined).length;\n  if (classes > 1) {\n    // Client-side twin of the host gate's three-way rule (R3-876): refuse before\n    // the wire so the ambiguous request is never in flight.\n    const err = new Error('requestEdit: give at most one of `path`, `file` or `bundleFile`') as CodedRefusalError;\n    err.code = 'invalid-params';\n    return Promise.reject(err);\n  }\n  return editorRequest('requestEdit', target ? { ...target } : {});\n};\n\n// ---------------------------------------------------------------------------\n// Editor SESSION management (EDITOR_AS_APP_SPEC §5.1; editor-as-app plan Phase\n// 03). Unlike `openInEditor` (the explorer's cross-app intent, `editor:open`),\n// these drive the editor's OWN open-tab set + active file, so they are gated by\n// the editor app's `editor:document` capability — a file explorer holding only\n// `editor:open` cannot call them. The host re-validates the path against the live\n// working tree; the editor itself stays host-owned (§2 recursion boundary).\n// ---------------------------------------------------------------------------\n\n/** An error from a session intent ({@link setActiveFile} / {@link closeFile}),\n *  carrying a machine-readable `.code`.\n *\n *  **What you actually catch.** The SDK throws a plain `Error` with `.code` assigned — a\n *  `CodedRefusalError` from `protocolRefusal.ts` — never a distinct class. This interface\n *  documents the `.code` VALUES the host sends. Nothing enforces the union at runtime —\n *  `.code` is whatever string arrived — so treat an unlisted code as possible and\n *  `instanceof Error` as the only reliable test.\n */\nexport interface EditorSessionError extends Error {\n  code:\n    | 'forbidden' // the frame lacks `editor:document`\n    | 'not-found' // no such file in the live working tree\n    | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n    | 'no-target' // there is no host editor session\n    | 'unknown';\n}\n\n/** Switch the editor's active file to `path`, opening it (adding a tab) if it is\n *  not already open — native `setActiveFile` parity. Rejects with an\n *  {@link EditorSessionError} (`.code`) if the path is missing/invalid or this app\n *  lacks `editor:document`. */\nexport const setActiveFile = (path: string): Promise<void> => editorRequest('setActive', { path });\n\n/** Close `path`'s tab in the editor (remove it from the open set) — native\n *  `closeFile` parity. Rejects with an {@link EditorSessionError} (`.code`). */\nexport const closeFile = (path: string): Promise<void> => editorRequest('close', { path });\n\n// ---------------------------------------------------------------------------\n// Working-tree mutation (UI_AS_APPS_SPEC §4 / EDITOR_AS_APP_SPEC §5.2). The file\n// explorer NAMES a working-tree path and the HOST performs the COW write (and\n// refreshes the preview) — the app holds no write port; it asks. Gated by the\n// first-party `editor:write` capability, so only a first-party chrome app (the\n// file explorer) can call these; anyone else is refused at the gate.\n// ---------------------------------------------------------------------------\n\n/** An error from a working-tree mutation, carrying a machine-readable `.code`.\n *\n *  **What you actually catch.** The SDK throws a plain `Error` with `.code` assigned — a\n *  `CodedRefusalError` from `protocolRefusal.ts` — never a distinct class. This interface\n *  documents the `.code` VALUES the host sends. Nothing enforces the union at runtime —\n *  `.code` is whatever string arrived — so treat an unlisted code as possible and\n *  `instanceof Error` as the only reliable test.\n */\nexport interface EditorWriteError extends Error {\n  /** R3-853: on a `too-large` refusal, the host's actual limit in bytes — the\n   *  host names the limit (LARGE_FILE_SUPPORT §3). The SDK forwards it verbatim\n   *  whenever a refusal carries it; the host sends it only on `too-large`. Absent\n   *  on an older host. */\n  limitBytes?: number;\n  code:\n    | 'forbidden' // the frame lacks `editor:write` (first-party-only)\n    | 'not-found' // the target file/folder does not exist (delete/rename)\n    | 'exists' // the target already exists (create/rename would clobber)\n    | 'protected' // the host refuses to delete this file (e.g. package.json)\n    | 'too-large' // an upload exceeds the host's size limit\n    | 'invalid-params' // a path was empty / contained `..` / looked like a URI\n    | 'no-target' // there is no host editor session\n    | 'unknown';\n}\n\n/** Create an empty working-tree file at `path` and open it. Rejects `exists` if a\n *  file is already there — see {@link EditorWriteError} for the full code list, which\n *  every helper below shares. */\nexport const createFile = (path: string): Promise<void> => editorRequest('createFile', { path });\n\n/** Create a working-tree folder at `path` (materialised with a `.gitkeep`). */\nexport const createFolder = (path: string): Promise<void> => editorRequest('createFolder', { path });\n\n/** Delete a working-tree file, or a folder and everything under it. Rejects\n *  `protected` for files the host won't remove, `not-found` if absent. */\nexport const deleteEntry = (path: string): Promise<void> => editorRequest('deleteEntry', { path });\n\n/** Rename/move a working-tree file from `from` to `to`. Rejects `exists` if `to`\n *  is taken, `not-found` if `from` is absent. */\nexport const renameEntry = (from: string, to: string): Promise<void> => editorRequest('rename', { from, to });\n\n/** Upload binary/text `bytes` to a working-tree file at `path`. Rejects\n *  `too-large` past the host's size limit — the rejection carries the limit as\n *  `err.limitBytes` (R3-853: the host names the limit; absent on an older host).\n *  The bytes are transferred (zero-copy). */\nexport const uploadFile = (path: string, bytes: Uint8Array): Promise<void> => editorRequest('upload', { path, bytes });\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAAgC;AAChC,6BAAuD;AACvD,6BAAwB;AACxB,sBAAgC;AAoChC,MAAM,gBAAgB,OAAO,QAAgB,QAAgD;AAC3F,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,+BAAe,GAAG,QAAQ,CAAC,GAAG,CAAC;AAC1E,MAAI;AACF,+CAAe,KAAK,UAAU,MAAM,SAAS;AAAA,EAC/C,SAAS,GAAG;AAMV,UAAM,QAAQ,OAAO,IAAI,OAAO,QAAQ,IAAI,aAAa;AACzD,QAAI,OAAO,UAAU,SAAU,CAAC,EAAuB,aAAa;AACpE,UAAM;AAAA,EACR;AACF;AAiDO,MAAM,eAAe,CAAC,MAAc,WAA6B,SACtE,cAAc,QAAQ;AAAA,EACpB;AAAA,EACA,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,EACjC,GAAI,MAAM,WAAW,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;AAClD,CAAC;AA6FI,MAAM,cAAc,CAAC,WAAuC;AACjE,QAAM,UAAU,CAAC,QAAQ,MAAM,QAAQ,MAAM,QAAQ,UAAU,EAAE,OAAO,CAAC,MAAM,MAAM,MAAS,EAAE;AAChG,MAAI,UAAU,GAAG;AAGf,UAAM,MAAM,IAAI,MAAM,iEAAiE;AACvF,QAAI,OAAO;AACX,WAAO,QAAQ,OAAO,GAAG;AAAA,EAC3B;AACA,SAAO,cAAc,eAAe,SAAS,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC;AACjE;AAiCO,MAAM,gBAAgB,CAAC,SAAgC,cAAc,aAAa,EAAE,KAAK,CAAC;AAI1F,MAAM,YAAY,CAAC,SAAgC,cAAc,SAAS,EAAE,KAAK,CAAC;AAsClF,MAAM,aAAa,CAAC,SAAgC,cAAc,cAAc,EAAE,KAAK,CAAC;AAGxF,MAAM,eAAe,CAAC,SAAgC,cAAc,gBAAgB,EAAE,KAAK,CAAC;AAI5F,MAAM,cAAc,CAAC,SAAgC,cAAc,eAAe,EAAE,KAAK,CAAC;AAI1F,MAAM,cAAc,CAAC,MAAc,OAA8B,cAAc,UAAU,EAAE,MAAM,GAAG,CAAC;AAMrG,MAAM,aAAa,CAAC,MAAc,UAAqC,cAAc,UAAU,EAAE,MAAM,MAAM,CAAC;","names":[]}