@agentproto/runtime 5.8.0 → 5.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@ import { AppUiBuildConfig } from '@agentproto/app-kit';
13
13
  * `uiPath` is missing, or older than the newest file matching `ui.build`'s
14
14
  * `sources` globs (default `["src/**"]`, resolved against `ui.build.cwd` —
15
15
  * default the app dir), it runs `ui.build.command` once, captures output to
16
- * `<appDir>/.agentproto/ui-build.log`, and returns a result the caller turns
16
+ * `~/.agentproto/logs/app-ui-build/` (see `appUiBuildLogPath`), and returns a result the caller turns
17
17
  * into either a served page or a readable error. Concurrent callers for the
18
18
  * same `uiPath` share one in-flight build (single-flight, keyed on the
19
19
  * absolute `uiPath` — an app's bundle lives at one path regardless of which
@@ -28,7 +28,13 @@ import { AppUiBuildConfig } from '@agentproto/app-kit';
28
28
  * `cwd`), or `undefined` if nothing matched any pattern. */
29
29
  declare function newestSourceMtime(cwd: string, patterns: readonly string[]): Promise<number | undefined>;
30
30
  /** Where a `ui.build` run's captured output lands — read this on a build
31
- * failure for the full log; the returned error carries only a tail. */
31
+ * failure for the full log; the returned error carries only a tail.
32
+ * Lives under the daemon state dir
33
+ * (`$AGENTPROTO_HOME|~/.agentproto/logs/app-ui-build/<name>-<hash>.log`),
34
+ * never inside the app dir: an installed app's dir is replaceable code
35
+ * (swapped on reinstall, packed into `.agentapp`s), not a place for
36
+ * runtime output. `<hash>` is over the absolute app dir, so two apps with
37
+ * the same folder name never share a log. */
32
38
  declare function appUiBuildLogPath(appDir: string): string;
33
39
  interface EnsureAppUiBuiltInput {
34
40
  /** Absolute app directory — anchors a relative `build.cwd` and the build
@@ -1,6 +1,8 @@
1
1
  import { spawn } from 'child_process';
2
+ import { createHash } from 'crypto';
2
3
  import { stat, readdir, mkdir, writeFile, readFile } from 'fs/promises';
3
- import { join, dirname, isAbsolute } from 'path';
4
+ import { homedir } from 'os';
5
+ import { resolve, join, basename, dirname, isAbsolute } from 'path';
4
6
 
5
7
  /**
6
8
  * @agentproto/runtime v0.1.0-alpha
@@ -106,7 +108,11 @@ async function newestSourceMtime(cwd, patterns) {
106
108
  var DEFAULT_BUILD_TIMEOUT_MS = 12e4;
107
109
  var DEFAULT_SOURCE_GLOBS = ["src/**"];
108
110
  function appUiBuildLogPath(appDir) {
109
- return join(appDir, ".agentproto", "ui-build.log");
111
+ const abs = resolve(appDir);
112
+ const home = process.env.AGENTPROTO_HOME ?? join(homedir(), ".agentproto");
113
+ const name = basename(abs).replace(/[^A-Za-z0-9._-]+/g, "-") || "app";
114
+ const hash = createHash("sha1").update(abs).digest("hex").slice(0, 12);
115
+ return join(home, "logs", "app-ui-build", `${name}-${hash}.log`);
110
116
  }
111
117
  function tailLines(text, n) {
112
118
  const lines = text.split("\n");
@@ -224,8 +230,8 @@ async function readLogTailSafe(dir, n) {
224
230
  async function resolveAppUiBuildState(input) {
225
231
  const { dir, uiPath } = input;
226
232
  const pending = peekInFlightBuild(uiPath) ?? ensureAppUiBuilt(input);
227
- const timeout = new Promise((resolve) => {
228
- const timer = setTimeout(() => resolve(RESOLVE_TIMEOUT), RESOLVE_FAST_PATH_MS);
233
+ const timeout = new Promise((resolve2) => {
234
+ const timer = setTimeout(() => resolve2(RESOLVE_TIMEOUT), RESOLVE_FAST_PATH_MS);
229
235
  timer.unref();
230
236
  });
231
237
  const raced = await Promise.race([pending, timeout]);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/app-ui-build.ts"],"names":["results","entries"],"mappings":"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","file":"app-ui-build.mjs","sourcesContent":["/**\n * Build an installed app's UI bundle on demand, per APP.md's declared\n * `ui.build` (`@agentproto/app-kit`'s `AppUiBuildConfig`) — the piece that\n * lets a repo declare \"how to build the UI\" instead of committing the\n * generated `.agentproto/ui/index.html`.\n *\n * `ensureAppUiBuilt` is the one entry point, called from every path that's\n * about to serve or install an app's UI (`app_install` in app-tools.ts,\n * `GET /apps/:appId/ui` in http-server.ts, the MCP panel cache in\n * app-ui-apps.ts, and the CLI's `agentproto app serve`): if the declared\n * `uiPath` is missing, or older than the newest file matching `ui.build`'s\n * `sources` globs (default `[\"src/**\"]`, resolved against `ui.build.cwd` —\n * default the app dir), it runs `ui.build.command` once, captures output to\n * `<appDir>/.agentproto/ui-build.log`, and returns a result the caller turns\n * into either a served page or a readable error. Concurrent callers for the\n * same `uiPath` share one in-flight build (single-flight, keyed on the\n * absolute `uiPath` — an app's bundle lives at one path regardless of which\n * route is asking).\n *\n * An app with no `ui.build` declared keeps today's behavior exactly: the\n * bundle must already exist on disk, and a missing one is a clear error\n * naming the path rather than a bare 404.\n */\n\nimport { spawn } from \"node:child_process\"\nimport { mkdir, readdir, readFile, stat, writeFile } from \"node:fs/promises\"\nimport type { Dirent, Stats } from \"node:fs\"\nimport { dirname, isAbsolute, join } from \"node:path\"\nimport type { AppUiBuildConfig } from \"@agentproto/app-kit\"\n\n/**\n * Deliberately NOT `command-tools.ts`'s `runCommand` — that pulls in\n * `sessions.ts` (session identity, spawn bookkeeping — thousands of lines\n * unrelated to running a build), which would drag the whole session\n * subsystem into this module's own `tsup` entry (`./app-ui-build`,\n * `tsup.config.ts`), a lean subpath the CLI imports on its own (same\n * pattern as `app-ui-delivery.ts`, which only imports node builtins). A\n * one-shot build command doesn't need `runCommand`'s process-group\n * bookkeeping either — it has no long-lived subtree to reap beyond its own\n * (possibly shell-spawned) child.\n */\ninterface RunResult {\n readonly exitCode: number\n readonly timedOut: boolean\n readonly stdout: string\n readonly stderr: string\n}\n\nfunction runShellCommand(command: string, cwd: string, timeoutMs: number): Promise<RunResult> {\n return new Promise(resolvePromise => {\n const child = spawn(command, {\n cwd,\n shell: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n })\n let stdout = \"\"\n let stderr = \"\"\n let timedOut = false\n child.stdout?.on(\"data\", d => {\n stdout += d.toString(\"utf8\")\n })\n child.stderr?.on(\"data\", d => {\n stderr += d.toString(\"utf8\")\n })\n const timer = setTimeout(() => {\n timedOut = true\n child.kill(\"SIGTERM\")\n setTimeout(() => child.kill(\"SIGKILL\"), 2_000).unref()\n }, timeoutMs)\n timer.unref()\n child.on(\"error\", err => {\n clearTimeout(timer)\n resolvePromise({ exitCode: -1, timedOut, stdout, stderr: stderr + (err as Error).message })\n })\n child.on(\"close\", code => {\n clearTimeout(timer)\n resolvePromise({ exitCode: code ?? -1, timedOut, stdout, stderr })\n })\n })\n}\n\nfunction resolveAgainst(dir: string, path: string): string {\n return isAbsolute(path) ? path : join(dir, path)\n}\n\n/** Turn one glob SEGMENT (no `/`) into a regex — `*` matches any run of\n * characters within the segment, everything else is literal. */\nfunction segmentToRegex(segment: string): RegExp {\n const escaped = segment.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\*/g, \".*\")\n return new RegExp(`^${escaped}$`)\n}\n\n/**\n * Resolve one glob pattern (already split into path segments, relative to\n * `baseDir`) to the absolute paths of every FILE it matches. Supports `**`\n * (zero or more path segments, so `\"src/**\"` matches every file anywhere\n * under `src/`, including `src/` itself) and `*` within a single segment —\n * not full glob syntax (no brace expansion, no negation, no `?`). That\n * subset covers the documented default (`src/**`) and the common overrides\n * (`package.json`, `public/**`, `*.html`) without pulling in a glob\n * dependency for a staleness check.\n */\nasync function collectGlobFiles(baseDir: string, segments: readonly string[]): Promise<string[]> {\n if (segments.length === 0) {\n try {\n const st = await stat(baseDir)\n return st.isFile() ? [baseDir] : []\n } catch {\n return []\n }\n }\n const [head, ...rest] = segments\n if (head === \"**\") {\n const results: string[] = [...(await collectGlobFiles(baseDir, rest))]\n let entries: Dirent[]\n try {\n entries = await readdir(baseDir, { withFileTypes: true })\n } catch {\n return results\n }\n for (const entry of entries) {\n const full = join(baseDir, entry.name)\n if (entry.isDirectory()) {\n results.push(...(await collectGlobFiles(full, segments)))\n } else if (rest.length === 0) {\n results.push(full)\n }\n }\n return results\n }\n const regex = segmentToRegex(head!)\n let entries: Dirent[]\n try {\n entries = await readdir(baseDir, { withFileTypes: true })\n } catch {\n return []\n }\n const results: string[] = []\n for (const entry of entries) {\n if (!regex.test(entry.name)) continue\n results.push(...(await collectGlobFiles(join(baseDir, entry.name), rest)))\n }\n return results\n}\n\n/** Newest mtime (ms) among every file matched by `patterns` (relative to\n * `cwd`), or `undefined` if nothing matched any pattern. */\nexport async function newestSourceMtime(\n cwd: string,\n patterns: readonly string[],\n): Promise<number | undefined> {\n let max: number | undefined\n for (const pattern of patterns) {\n const segments = pattern.replace(/^\\.\\//, \"\").split(\"/\").filter(s => s.length > 0)\n if (segments.length === 0) continue\n for (const file of await collectGlobFiles(cwd, segments)) {\n try {\n const st = await stat(file)\n if (max === undefined || st.mtimeMs > max) max = st.mtimeMs\n } catch {\n // Vanished between listing and stat — ignore.\n }\n }\n }\n return max\n}\n\nconst DEFAULT_BUILD_TIMEOUT_MS = 120_000\nconst DEFAULT_SOURCE_GLOBS = [\"src/**\"]\nconst LOG_TAIL_LINES = 40\n\n/** Where a `ui.build` run's captured output lands — read this on a build\n * failure for the full log; the returned error carries only a tail. */\nexport function appUiBuildLogPath(appDir: string): string {\n return join(appDir, \".agentproto\", \"ui-build.log\")\n}\n\nfunction tailLines(text: string, n: number): string {\n const lines = text.split(\"\\n\")\n return lines.slice(-n).join(\"\\n\")\n}\n\n/** A built single-file bundle should never reference `./assets/…` — the\n * daemon serves `ui.path` standalone (no adjacent static server for a\n * split build's chunks outside the one `assets/` convention `GET\n * /apps/:appId/ui/assets/:file` already covers for a *committed* bundle).\n * A build step that emits a split bundle anyway gets a loud warning, not a\n * failure — the app may still work if its bundler also copied the chunks\n * next to `ui.path`. */\nconst ASSET_REFERENCE_RE = /(?:src|href)\\s*=\\s*[\"'](?:\\.{0,2}\\/)?assets\\//i\n\nasync function warnIfNotSingleFile(uiPath: string): Promise<void> {\n let html: string\n try {\n html = await readFile(uiPath, \"utf8\")\n } catch {\n return\n }\n if (ASSET_REFERENCE_RE.test(html)) {\n console.warn(\n `[app-ui-build] \"${uiPath}\" references \"./assets\" after a ui.build run. ` +\n \"The daemon expects a single-file bundle here — a split build (separate \" +\n \"JS/CSS chunks under an assets/ dir) may 404 those chunks unless the \" +\n \"app's bundler inlines everything into one file.\",\n )\n }\n}\n\nexport interface EnsureAppUiBuiltInput {\n /** Absolute app directory — anchors a relative `build.cwd` and the build\n * log's location. */\n readonly dir: string\n /** Absolute path to the ui bundle's entry file (APP.md's `ui.path`,\n * already resolved absolute). Also the single-flight key. */\n readonly uiPath: string\n readonly build?: AppUiBuildConfig\n /** Hard timeout for the build command. Defaults to 120s. */\n readonly timeoutMs?: number\n}\n\nexport type EnsureAppUiBuiltResult =\n | { readonly ok: true; readonly built: boolean }\n | { readonly ok: false; readonly error: string }\n\n/** In-flight builds keyed on the absolute `uiPath` — an app's bundle lives\n * at one path regardless of which route (app_install, GET .../ui, the MCP\n * panel cache, `app serve`) asked for it, so keying here (rather than on\n * appId, which isn't always known yet — see app-tools.ts's `performInstall`\n * calling this BEFORE the app has an id) still dedupes every caller. */\nconst inFlight = new Map<string, Promise<EnsureAppUiBuiltResult>>()\n\n/** When the CURRENTLY in-flight build (if any) actually started running its\n * `build.command` — set right before `runShellCommand`, read by a\n * non-blocking caller (`resolveAppUiBuildState` below) to render \"building\n * for Ns\" without itself timing anything. Not set for a call that resolves\n * from `existing`/freshness alone (no command ever ran). */\nconst buildStartedAt = new Map<string, number>()\n\n/** Peek whether `uiPath` has a build running right now without starting one\n * — a non-blocking caller (`resolveAppUiBuildState`) uses this to join an\n * ALREADY-showing build instead of racing a fresh `ensureAppUiBuilt` call\n * (which would itself re-do the cheap stat/freshness check every time). */\nexport function peekInFlightBuild(uiPath: string): Promise<EnsureAppUiBuiltResult> | undefined {\n return inFlight.get(uiPath)\n}\n\n/** Paired with {@link peekInFlightBuild}: when that returns a promise, this\n * is the wall-clock time (`Date.now()`) its build command actually started\n * — `undefined` if the in-flight promise hasn't reached `runShellCommand`\n * yet (a handful of stat calls out), in which case \"now\" is as good a\n * start time as any. */\nexport function peekBuildStartedAt(uiPath: string): number | undefined {\n return buildStartedAt.get(uiPath)\n}\n\nasync function runEnsure(input: EnsureAppUiBuiltInput): Promise<EnsureAppUiBuiltResult> {\n const { dir, uiPath, build } = input\n\n let existing: Stats | undefined\n try {\n existing = await stat(uiPath)\n } catch {\n existing = undefined\n }\n\n if (!build) {\n if (existing) return { ok: true, built: false }\n return {\n ok: false,\n error:\n `app ui bundle \"${uiPath}\" is missing and the app declares no \\`ui.build\\` — ` +\n \"either commit the built bundle, or add a `ui.build` step to APP.md's `ui` \" +\n \"frontmatter (`ui.build.command`, run with cwd `ui.build.cwd` or the app dir).\",\n }\n }\n\n const buildCwd = build.cwd ? resolveAgainst(dir, build.cwd) : dir\n const sources = build.sources ?? DEFAULT_SOURCE_GLOBS\n\n if (existing) {\n const newest = await newestSourceMtime(buildCwd, sources)\n if (newest === undefined || newest <= existing.mtimeMs) {\n return { ok: true, built: false }\n }\n }\n\n const logPath = appUiBuildLogPath(dir)\n const startedAt = new Date().toISOString()\n buildStartedAt.set(uiPath, Date.now())\n const result = await runShellCommand(build.command, buildCwd, input.timeoutMs ?? DEFAULT_BUILD_TIMEOUT_MS)\n\n const logBody =\n `[${startedAt}] ui.build: ${build.command}\\n` +\n `cwd: ${buildCwd}\\n` +\n `exit: ${result.exitCode}${result.timedOut ? \" (timed out)\" : \"\"}\\n\\n` +\n `--- stdout ---\\n${result.stdout}\\n--- stderr ---\\n${result.stderr}\\n`\n try {\n await mkdir(dirname(logPath), { recursive: true })\n await writeFile(logPath, logBody, \"utf8\")\n } catch {\n // Best-effort — a log write failure must not hide a build failure.\n }\n\n if (result.exitCode !== 0) {\n return {\n ok: false,\n error:\n `ui.build command \"${build.command}\" failed (exit ${result.exitCode}` +\n `${result.timedOut ? \", timed out\" : \"\"}). See \"${logPath}\" for full output. ` +\n `Last lines:\\n${tailLines(result.stderr || result.stdout, LOG_TAIL_LINES)}`,\n }\n }\n\n let rebuilt: Stats | undefined\n try {\n rebuilt = await stat(uiPath)\n } catch {\n rebuilt = undefined\n }\n if (!rebuilt) {\n return {\n ok: false,\n error:\n `ui.build command \"${build.command}\" exited 0 but \"${uiPath}\" is still missing — ` +\n `check the command actually writes its output there. See \"${logPath}\" for full output.`,\n }\n }\n\n await warnIfNotSingleFile(uiPath)\n\n return { ok: true, built: true }\n}\n\n/**\n * Ensure `uiPath` exists and is at least as new as its declared sources,\n * building it first if not. Single-flight per `uiPath`: a second call for\n * the same path while a build is running awaits the same result instead of\n * starting a second build.\n */\nexport async function ensureAppUiBuilt(\n input: EnsureAppUiBuiltInput,\n): Promise<EnsureAppUiBuiltResult> {\n const key = input.uiPath\n const inflight = inFlight.get(key)\n if (inflight) return inflight\n const promise = runEnsure(input).finally(() => {\n inFlight.delete(key)\n buildStartedAt.delete(key)\n })\n inFlight.set(key, promise)\n return promise\n}\n\nconst RESOLVE_FAST_PATH_MS = 300\nconst READABLE_LOG_TAIL_LINES = 20\nconst RESOLVE_TIMEOUT = Symbol(\"app-ui-build-resolve-timeout\")\n\nasync function readLogTailSafe(dir: string, n: number): Promise<string | undefined> {\n try {\n const text = await readFile(appUiBuildLogPath(dir), \"utf8\")\n const lines = text.split(\"\\n\").filter(line => line.length > 0)\n const tail = lines.slice(-n).join(\"\\n\")\n return tail.length > 0 ? tail : undefined\n } catch {\n return undefined\n }\n}\n\nexport type AppUiBuildState =\n | { readonly kind: \"ready\" }\n | { readonly kind: \"building\"; readonly startedAt: number; readonly logTail?: string }\n | { readonly kind: \"error\"; readonly message: string; readonly logPath: string; readonly logTail?: string }\n\n/**\n * Non-blocking status check for `ensureAppUiBuilt`'s outcome — the piece\n * that lets a PAGE render (the `ui://app_ui_<id>/view` MCP resource in\n * app-ui-apps.ts, `GET /apps/:appId/ui` in http-server.ts) never stall on a\n * multi-second `ui.build` run. Joins an already-running build via\n * {@link peekInFlightBuild} (or starts one — still single-flight, since\n * `ensureAppUiBuilt` itself owns `inFlight`) and waits only up to\n * `RESOLVE_FAST_PATH_MS`: long enough that the common \"already built and\n * fresh\" case (a handful of `stat` calls) resolves as `\"ready\"` without ever\n * showing a placeholder, short enough that a real build never blocks the\n * caller — it keeps running in the background regardless, and the NEXT call\n * (after the page's own reload) picks up wherever that build landed.\n */\nexport async function resolveAppUiBuildState(input: EnsureAppUiBuiltInput): Promise<AppUiBuildState> {\n const { dir, uiPath } = input\n const pending = peekInFlightBuild(uiPath) ?? ensureAppUiBuilt(input)\n\n const timeout = new Promise<typeof RESOLVE_TIMEOUT>(resolve => {\n const timer = setTimeout(() => resolve(RESOLVE_TIMEOUT), RESOLVE_FAST_PATH_MS)\n timer.unref()\n })\n const raced = await Promise.race([pending, timeout])\n\n if (raced === RESOLVE_TIMEOUT) {\n const startedAt = peekBuildStartedAt(uiPath) ?? Date.now()\n return { kind: \"building\", startedAt, logTail: await readLogTailSafe(dir, READABLE_LOG_TAIL_LINES) }\n }\n const result = raced as EnsureAppUiBuiltResult\n if (!result.ok) {\n return {\n kind: \"error\",\n message: result.error,\n logPath: appUiBuildLogPath(dir),\n logTail: await readLogTailSafe(dir, READABLE_LOG_TAIL_LINES),\n }\n }\n return { kind: \"ready\" }\n}\n"]}
1
+ {"version":3,"sources":["../src/app-ui-build.ts"],"names":["results","entries","resolve"],"mappings":"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","file":"app-ui-build.mjs","sourcesContent":["/**\n * Build an installed app's UI bundle on demand, per APP.md's declared\n * `ui.build` (`@agentproto/app-kit`'s `AppUiBuildConfig`) — the piece that\n * lets a repo declare \"how to build the UI\" instead of committing the\n * generated `.agentproto/ui/index.html`.\n *\n * `ensureAppUiBuilt` is the one entry point, called from every path that's\n * about to serve or install an app's UI (`app_install` in app-tools.ts,\n * `GET /apps/:appId/ui` in http-server.ts, the MCP panel cache in\n * app-ui-apps.ts, and the CLI's `agentproto app serve`): if the declared\n * `uiPath` is missing, or older than the newest file matching `ui.build`'s\n * `sources` globs (default `[\"src/**\"]`, resolved against `ui.build.cwd` —\n * default the app dir), it runs `ui.build.command` once, captures output to\n * `~/.agentproto/logs/app-ui-build/` (see `appUiBuildLogPath`), and returns a result the caller turns\n * into either a served page or a readable error. Concurrent callers for the\n * same `uiPath` share one in-flight build (single-flight, keyed on the\n * absolute `uiPath` — an app's bundle lives at one path regardless of which\n * route is asking).\n *\n * An app with no `ui.build` declared keeps today's behavior exactly: the\n * bundle must already exist on disk, and a missing one is a clear error\n * naming the path rather than a bare 404.\n */\n\nimport { spawn } from \"node:child_process\"\nimport { createHash } from \"node:crypto\"\nimport { mkdir, readdir, readFile, stat, writeFile } from \"node:fs/promises\"\nimport type { Dirent, Stats } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { basename, dirname, isAbsolute, join, resolve } from \"node:path\"\nimport type { AppUiBuildConfig } from \"@agentproto/app-kit\"\n\n/**\n * Deliberately NOT `command-tools.ts`'s `runCommand` — that pulls in\n * `sessions.ts` (session identity, spawn bookkeeping — thousands of lines\n * unrelated to running a build), which would drag the whole session\n * subsystem into this module's own `tsup` entry (`./app-ui-build`,\n * `tsup.config.ts`), a lean subpath the CLI imports on its own (same\n * pattern as `app-ui-delivery.ts`, which only imports node builtins). A\n * one-shot build command doesn't need `runCommand`'s process-group\n * bookkeeping either — it has no long-lived subtree to reap beyond its own\n * (possibly shell-spawned) child.\n */\ninterface RunResult {\n readonly exitCode: number\n readonly timedOut: boolean\n readonly stdout: string\n readonly stderr: string\n}\n\nfunction runShellCommand(command: string, cwd: string, timeoutMs: number): Promise<RunResult> {\n return new Promise(resolvePromise => {\n const child = spawn(command, {\n cwd,\n shell: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n })\n let stdout = \"\"\n let stderr = \"\"\n let timedOut = false\n child.stdout?.on(\"data\", d => {\n stdout += d.toString(\"utf8\")\n })\n child.stderr?.on(\"data\", d => {\n stderr += d.toString(\"utf8\")\n })\n const timer = setTimeout(() => {\n timedOut = true\n child.kill(\"SIGTERM\")\n setTimeout(() => child.kill(\"SIGKILL\"), 2_000).unref()\n }, timeoutMs)\n timer.unref()\n child.on(\"error\", err => {\n clearTimeout(timer)\n resolvePromise({ exitCode: -1, timedOut, stdout, stderr: stderr + (err as Error).message })\n })\n child.on(\"close\", code => {\n clearTimeout(timer)\n resolvePromise({ exitCode: code ?? -1, timedOut, stdout, stderr })\n })\n })\n}\n\nfunction resolveAgainst(dir: string, path: string): string {\n return isAbsolute(path) ? path : join(dir, path)\n}\n\n/** Turn one glob SEGMENT (no `/`) into a regex — `*` matches any run of\n * characters within the segment, everything else is literal. */\nfunction segmentToRegex(segment: string): RegExp {\n const escaped = segment.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\*/g, \".*\")\n return new RegExp(`^${escaped}$`)\n}\n\n/**\n * Resolve one glob pattern (already split into path segments, relative to\n * `baseDir`) to the absolute paths of every FILE it matches. Supports `**`\n * (zero or more path segments, so `\"src/**\"` matches every file anywhere\n * under `src/`, including `src/` itself) and `*` within a single segment —\n * not full glob syntax (no brace expansion, no negation, no `?`). That\n * subset covers the documented default (`src/**`) and the common overrides\n * (`package.json`, `public/**`, `*.html`) without pulling in a glob\n * dependency for a staleness check.\n */\nasync function collectGlobFiles(baseDir: string, segments: readonly string[]): Promise<string[]> {\n if (segments.length === 0) {\n try {\n const st = await stat(baseDir)\n return st.isFile() ? [baseDir] : []\n } catch {\n return []\n }\n }\n const [head, ...rest] = segments\n if (head === \"**\") {\n const results: string[] = [...(await collectGlobFiles(baseDir, rest))]\n let entries: Dirent[]\n try {\n entries = await readdir(baseDir, { withFileTypes: true })\n } catch {\n return results\n }\n for (const entry of entries) {\n const full = join(baseDir, entry.name)\n if (entry.isDirectory()) {\n results.push(...(await collectGlobFiles(full, segments)))\n } else if (rest.length === 0) {\n results.push(full)\n }\n }\n return results\n }\n const regex = segmentToRegex(head!)\n let entries: Dirent[]\n try {\n entries = await readdir(baseDir, { withFileTypes: true })\n } catch {\n return []\n }\n const results: string[] = []\n for (const entry of entries) {\n if (!regex.test(entry.name)) continue\n results.push(...(await collectGlobFiles(join(baseDir, entry.name), rest)))\n }\n return results\n}\n\n/** Newest mtime (ms) among every file matched by `patterns` (relative to\n * `cwd`), or `undefined` if nothing matched any pattern. */\nexport async function newestSourceMtime(\n cwd: string,\n patterns: readonly string[],\n): Promise<number | undefined> {\n let max: number | undefined\n for (const pattern of patterns) {\n const segments = pattern.replace(/^\\.\\//, \"\").split(\"/\").filter(s => s.length > 0)\n if (segments.length === 0) continue\n for (const file of await collectGlobFiles(cwd, segments)) {\n try {\n const st = await stat(file)\n if (max === undefined || st.mtimeMs > max) max = st.mtimeMs\n } catch {\n // Vanished between listing and stat — ignore.\n }\n }\n }\n return max\n}\n\nconst DEFAULT_BUILD_TIMEOUT_MS = 120_000\nconst DEFAULT_SOURCE_GLOBS = [\"src/**\"]\nconst LOG_TAIL_LINES = 40\n\n/** Where a `ui.build` run's captured output lands — read this on a build\n * failure for the full log; the returned error carries only a tail.\n * Lives under the daemon state dir\n * (`$AGENTPROTO_HOME|~/.agentproto/logs/app-ui-build/<name>-<hash>.log`),\n * never inside the app dir: an installed app's dir is replaceable code\n * (swapped on reinstall, packed into `.agentapp`s), not a place for\n * runtime output. `<hash>` is over the absolute app dir, so two apps with\n * the same folder name never share a log. */\nexport function appUiBuildLogPath(appDir: string): string {\n const abs = resolve(appDir)\n const home = process.env.AGENTPROTO_HOME ?? join(homedir(), \".agentproto\")\n const name = basename(abs).replace(/[^A-Za-z0-9._-]+/g, \"-\") || \"app\"\n const hash = createHash(\"sha1\").update(abs).digest(\"hex\").slice(0, 12)\n return join(home, \"logs\", \"app-ui-build\", `${name}-${hash}.log`)\n}\n\nfunction tailLines(text: string, n: number): string {\n const lines = text.split(\"\\n\")\n return lines.slice(-n).join(\"\\n\")\n}\n\n/** A built single-file bundle should never reference `./assets/…` — the\n * daemon serves `ui.path` standalone (no adjacent static server for a\n * split build's chunks outside the one `assets/` convention `GET\n * /apps/:appId/ui/assets/:file` already covers for a *committed* bundle).\n * A build step that emits a split bundle anyway gets a loud warning, not a\n * failure — the app may still work if its bundler also copied the chunks\n * next to `ui.path`. */\nconst ASSET_REFERENCE_RE = /(?:src|href)\\s*=\\s*[\"'](?:\\.{0,2}\\/)?assets\\//i\n\nasync function warnIfNotSingleFile(uiPath: string): Promise<void> {\n let html: string\n try {\n html = await readFile(uiPath, \"utf8\")\n } catch {\n return\n }\n if (ASSET_REFERENCE_RE.test(html)) {\n console.warn(\n `[app-ui-build] \"${uiPath}\" references \"./assets\" after a ui.build run. ` +\n \"The daemon expects a single-file bundle here — a split build (separate \" +\n \"JS/CSS chunks under an assets/ dir) may 404 those chunks unless the \" +\n \"app's bundler inlines everything into one file.\",\n )\n }\n}\n\nexport interface EnsureAppUiBuiltInput {\n /** Absolute app directory — anchors a relative `build.cwd` and the build\n * log's location. */\n readonly dir: string\n /** Absolute path to the ui bundle's entry file (APP.md's `ui.path`,\n * already resolved absolute). Also the single-flight key. */\n readonly uiPath: string\n readonly build?: AppUiBuildConfig\n /** Hard timeout for the build command. Defaults to 120s. */\n readonly timeoutMs?: number\n}\n\nexport type EnsureAppUiBuiltResult =\n | { readonly ok: true; readonly built: boolean }\n | { readonly ok: false; readonly error: string }\n\n/** In-flight builds keyed on the absolute `uiPath` — an app's bundle lives\n * at one path regardless of which route (app_install, GET .../ui, the MCP\n * panel cache, `app serve`) asked for it, so keying here (rather than on\n * appId, which isn't always known yet — see app-tools.ts's `performInstall`\n * calling this BEFORE the app has an id) still dedupes every caller. */\nconst inFlight = new Map<string, Promise<EnsureAppUiBuiltResult>>()\n\n/** When the CURRENTLY in-flight build (if any) actually started running its\n * `build.command` — set right before `runShellCommand`, read by a\n * non-blocking caller (`resolveAppUiBuildState` below) to render \"building\n * for Ns\" without itself timing anything. Not set for a call that resolves\n * from `existing`/freshness alone (no command ever ran). */\nconst buildStartedAt = new Map<string, number>()\n\n/** Peek whether `uiPath` has a build running right now without starting one\n * — a non-blocking caller (`resolveAppUiBuildState`) uses this to join an\n * ALREADY-showing build instead of racing a fresh `ensureAppUiBuilt` call\n * (which would itself re-do the cheap stat/freshness check every time). */\nexport function peekInFlightBuild(uiPath: string): Promise<EnsureAppUiBuiltResult> | undefined {\n return inFlight.get(uiPath)\n}\n\n/** Paired with {@link peekInFlightBuild}: when that returns a promise, this\n * is the wall-clock time (`Date.now()`) its build command actually started\n * — `undefined` if the in-flight promise hasn't reached `runShellCommand`\n * yet (a handful of stat calls out), in which case \"now\" is as good a\n * start time as any. */\nexport function peekBuildStartedAt(uiPath: string): number | undefined {\n return buildStartedAt.get(uiPath)\n}\n\nasync function runEnsure(input: EnsureAppUiBuiltInput): Promise<EnsureAppUiBuiltResult> {\n const { dir, uiPath, build } = input\n\n let existing: Stats | undefined\n try {\n existing = await stat(uiPath)\n } catch {\n existing = undefined\n }\n\n if (!build) {\n if (existing) return { ok: true, built: false }\n return {\n ok: false,\n error:\n `app ui bundle \"${uiPath}\" is missing and the app declares no \\`ui.build\\` — ` +\n \"either commit the built bundle, or add a `ui.build` step to APP.md's `ui` \" +\n \"frontmatter (`ui.build.command`, run with cwd `ui.build.cwd` or the app dir).\",\n }\n }\n\n const buildCwd = build.cwd ? resolveAgainst(dir, build.cwd) : dir\n const sources = build.sources ?? DEFAULT_SOURCE_GLOBS\n\n if (existing) {\n const newest = await newestSourceMtime(buildCwd, sources)\n if (newest === undefined || newest <= existing.mtimeMs) {\n return { ok: true, built: false }\n }\n }\n\n const logPath = appUiBuildLogPath(dir)\n const startedAt = new Date().toISOString()\n buildStartedAt.set(uiPath, Date.now())\n const result = await runShellCommand(build.command, buildCwd, input.timeoutMs ?? DEFAULT_BUILD_TIMEOUT_MS)\n\n const logBody =\n `[${startedAt}] ui.build: ${build.command}\\n` +\n `cwd: ${buildCwd}\\n` +\n `exit: ${result.exitCode}${result.timedOut ? \" (timed out)\" : \"\"}\\n\\n` +\n `--- stdout ---\\n${result.stdout}\\n--- stderr ---\\n${result.stderr}\\n`\n try {\n await mkdir(dirname(logPath), { recursive: true })\n await writeFile(logPath, logBody, \"utf8\")\n } catch {\n // Best-effort — a log write failure must not hide a build failure.\n }\n\n if (result.exitCode !== 0) {\n return {\n ok: false,\n error:\n `ui.build command \"${build.command}\" failed (exit ${result.exitCode}` +\n `${result.timedOut ? \", timed out\" : \"\"}). See \"${logPath}\" for full output. ` +\n `Last lines:\\n${tailLines(result.stderr || result.stdout, LOG_TAIL_LINES)}`,\n }\n }\n\n let rebuilt: Stats | undefined\n try {\n rebuilt = await stat(uiPath)\n } catch {\n rebuilt = undefined\n }\n if (!rebuilt) {\n return {\n ok: false,\n error:\n `ui.build command \"${build.command}\" exited 0 but \"${uiPath}\" is still missing — ` +\n `check the command actually writes its output there. See \"${logPath}\" for full output.`,\n }\n }\n\n await warnIfNotSingleFile(uiPath)\n\n return { ok: true, built: true }\n}\n\n/**\n * Ensure `uiPath` exists and is at least as new as its declared sources,\n * building it first if not. Single-flight per `uiPath`: a second call for\n * the same path while a build is running awaits the same result instead of\n * starting a second build.\n */\nexport async function ensureAppUiBuilt(\n input: EnsureAppUiBuiltInput,\n): Promise<EnsureAppUiBuiltResult> {\n const key = input.uiPath\n const inflight = inFlight.get(key)\n if (inflight) return inflight\n const promise = runEnsure(input).finally(() => {\n inFlight.delete(key)\n buildStartedAt.delete(key)\n })\n inFlight.set(key, promise)\n return promise\n}\n\nconst RESOLVE_FAST_PATH_MS = 300\nconst READABLE_LOG_TAIL_LINES = 20\nconst RESOLVE_TIMEOUT = Symbol(\"app-ui-build-resolve-timeout\")\n\nasync function readLogTailSafe(dir: string, n: number): Promise<string | undefined> {\n try {\n const text = await readFile(appUiBuildLogPath(dir), \"utf8\")\n const lines = text.split(\"\\n\").filter(line => line.length > 0)\n const tail = lines.slice(-n).join(\"\\n\")\n return tail.length > 0 ? tail : undefined\n } catch {\n return undefined\n }\n}\n\nexport type AppUiBuildState =\n | { readonly kind: \"ready\" }\n | { readonly kind: \"building\"; readonly startedAt: number; readonly logTail?: string }\n | { readonly kind: \"error\"; readonly message: string; readonly logPath: string; readonly logTail?: string }\n\n/**\n * Non-blocking status check for `ensureAppUiBuilt`'s outcome — the piece\n * that lets a PAGE render (the `ui://app_ui_<id>/view` MCP resource in\n * app-ui-apps.ts, `GET /apps/:appId/ui` in http-server.ts) never stall on a\n * multi-second `ui.build` run. Joins an already-running build via\n * {@link peekInFlightBuild} (or starts one — still single-flight, since\n * `ensureAppUiBuilt` itself owns `inFlight`) and waits only up to\n * `RESOLVE_FAST_PATH_MS`: long enough that the common \"already built and\n * fresh\" case (a handful of `stat` calls) resolves as `\"ready\"` without ever\n * showing a placeholder, short enough that a real build never blocks the\n * caller — it keeps running in the background regardless, and the NEXT call\n * (after the page's own reload) picks up wherever that build landed.\n */\nexport async function resolveAppUiBuildState(input: EnsureAppUiBuiltInput): Promise<AppUiBuildState> {\n const { dir, uiPath } = input\n const pending = peekInFlightBuild(uiPath) ?? ensureAppUiBuilt(input)\n\n const timeout = new Promise<typeof RESOLVE_TIMEOUT>(resolve => {\n const timer = setTimeout(() => resolve(RESOLVE_TIMEOUT), RESOLVE_FAST_PATH_MS)\n timer.unref()\n })\n const raced = await Promise.race([pending, timeout])\n\n if (raced === RESOLVE_TIMEOUT) {\n const startedAt = peekBuildStartedAt(uiPath) ?? Date.now()\n return { kind: \"building\", startedAt, logTail: await readLogTailSafe(dir, READABLE_LOG_TAIL_LINES) }\n }\n const result = raced as EnsureAppUiBuiltResult\n if (!result.ok) {\n return {\n kind: \"error\",\n message: result.error,\n logPath: appUiBuildLogPath(dir),\n logTail: await readLogTailSafe(dir, READABLE_LOG_TAIL_LINES),\n }\n }\n return { kind: \"ready\" }\n}\n"]}
@@ -47,7 +47,7 @@ interface AppUiBuildingHtmlInput {
47
47
  * counter is computed client-side off this, not baked in as static text,
48
48
  * so it keeps ticking between reloads. */
49
49
  readonly startedAt: number;
50
- /** Last lines of `.agentproto/ui-build.log`, if cheap to read. */
50
+ /** Last lines of the app's ui-build log (`appUiBuildLogPath`), if cheap to read. */
51
51
  readonly logTail?: string;
52
52
  }
53
53
  /** The "still building" placeholder — see the module doc for the reload
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/app-ui-placeholder.ts"],"names":[],"mappings":";;;;;;AA2CO,IAAM,2BAAA,GAA8B;AAE3C,IAAM,eAAA,GAAkB,CAAA;AAExB,SAAS,WAAW,IAAA,EAAsB;AACxC,EAAA,OAAO,IAAA,CACJ,OAAA,CAAQ,IAAA,EAAM,OAAO,EACrB,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA,CACpB,QAAQ,IAAA,EAAM,MAAM,CAAA,CACpB,OAAA,CAAQ,MAAM,QAAQ,CAAA;AAC3B;AAEA,SAAS,UAAU,IAAA,EAMR;AACT,EAAA,OAAO,CAAA;AAAA,eAAA,EACQ,KAAK,UAAA,GAAa,CAAA,CAAA,EAAI,IAAA,CAAK,UAAU,KAAK,EAAE,CAAA;AAAA;AAAA;AAAA,EAG3D,IAAA,CAAK,OAAA,GAAU,CAAA,oCAAA,EAAuC,eAAe,OAAO,EAAE;AAAA,OAAA,EACvE,UAAA,CAAW,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4B7B,KAAK,IAAI;AAAA,EACT,KAAK,MAAA,GAAS,CAAA,QAAA,EAAW,IAAA,CAAK,MAAM,cAAc,EAAE;AAAA;AAAA,OAAA,CAAA;AAGtD;AAeO,SAAS,wBAAwB,KAAA,EAAuC;AAC7E,EAAA,MAAM,IAAA,GAAO;AAAA;AAAA,eAAA,EAEE,UAAA,CAAW,KAAA,CAAM,OAAO,CAAC,CAAA;AAAA;AAAA,EAAA,EAEtC,KAAA,CAAM,UAAU,CAAA,KAAA,EAAQ,UAAA,CAAW,MAAM,OAAO,CAAC,WAAW,EAAE,CAAA,CAAA;AAChE,EAAA,MAAM,MAAA,GAAS,CAAA;AAAA,kBAAA,EACG,IAAA,CAAK,SAAA,CAAU,KAAA,CAAM,SAAS,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAAA,CAAA;AAUjD,EAAA,OAAO,SAAA,CAAU;AAAA,IACf,UAAA,EAAY,2BAAA;AAAA,IACZ,OAAA,EAAS,IAAA;AAAA,IACT,KAAA,EAAO,CAAA,SAAA,EAAY,KAAA,CAAM,OAAO,CAAA,MAAA,CAAA;AAAA,IAChC,IAAA;AAAA,IACA;AAAA,GACD,CAAA;AACH;AAiBO,SAAS,qBAAqB,KAAA,EAAoC;AACvE,EAAA,MAAM,GAAA,GACJ,MAAM,GAAA,IACN,6EAAA;AACF,EAAA,MAAM,IAAA,GAAO;AAAA,MAAA,EACP,UAAA,CAAW,KAAA,CAAM,OAAO,CAAC,CAAA;AAAA,KAAA,EAC1B,UAAA,CAAW,KAAA,CAAM,OAAO,CAAC,CAAA;AAAA,EAAA,EAC5B,KAAA,CAAM,aAAa,CAAA,SAAA,EAAY,UAAA,CAAW,MAAM,UAAU,CAAC,gBAAgB,EAAE;AAAA,KAAA,EAC1E,UAAA,CAAW,GAAG,CAAC,CAAA;AAAA,EAAA,EAClB,KAAA,CAAM,UAAU,CAAA,KAAA,EAAQ,UAAA,CAAW,MAAM,OAAO,CAAC,WAAW,EAAE,CAAA,CAAA;AAChE,EAAA,OAAO,SAAA,CAAU,EAAE,KAAA,EAAO,CAAA,EAAG,MAAM,OAAO,CAAA,gBAAA,CAAA,EAAoB,MAAM,CAAA;AACtE","file":"app-ui-placeholder.mjs","sourcesContent":["/**\n * Self-contained HTML pages served in place of an installed app's real UI\n * bundle while it's building, or when it can never be served (missing file\n * with no `ui.build`, a failed build, a removed app dir). Both surfaces that\n * serve an app's UI as a PAGE — the `ui://app_ui_<id>/view` MCP resource\n * (app-ui-apps.ts) and `GET /apps/:appId/ui` (http-server.ts) — render\n * through here instead of ever putting raw `{\"error\":...}` JSON in a page\n * body. That JSON shape is kept exactly as-is for MCP TOOL callers (e.g.\n * `app_install`'s error result) — this module is for the two PAGE routes\n * only.\n *\n * Reload strategy — deliberately two different mechanisms for the two\n * hosts, because neither works everywhere:\n *\n * - The VS Code webview panel (packages/vscode appPanel.ts) embeds this\n * page as an inner `srcdoc` iframe of an outer document it fully\n * controls (buildAppHostHtml). `srcdoc` has no URL, so a navigation\n * inside it (meta-refresh) just re-parses the SAME string — it can\n * never pick up new content on its own. The extension host is what can:\n * appPanel.ts detects `BUILDING_STATUS_ATTR` on the returned html and\n * re-issues `resources/read` on a timer, replacing `panel.webview.html`\n * wholesale until the marker is gone (see appPanel.logic.ts's\n * `isAppUiBuilding`).\n * - `GET /apps/:appId/ui` (http-server.ts) IS a real top-level navigation\n * target, so this page's own `<meta http-equiv=\"refresh\">` re-requests\n * the same URL from the server directly — no host cooperation needed.\n *\n * Both mechanisms are outside this module's own script, which is\n * deliberate: the page also has to survive the strictest CSP either host\n * imposes — the VS Code outer document sets `default-src 'none'; script-src\n * 'unsafe-inline'; style-src 'unsafe-inline'` with NO `connect-src`, and the\n * srcdoc iframe inherits it verbatim, so nothing here may `fetch`, open a\n * `WebSocket`/`EventSource`, or load an external image/font. The one bit of\n * live JS this page does carry — the elapsed-time counter — only touches\n * `Date.now()` and the DOM off a baked-in timestamp, neither of which needs\n * network access.\n */\n\n/** `data-agentproto-ui-status` value while a build is in flight (or about to\n * start) — `appPanel.logic.ts`'s `isAppUiBuilding` matches on this exact\n * attribute/value pair; keep the two in lockstep (same cross-package\n * duplication the file's own `appUiToolId` already accepts, since a\n * `ui://`-served page can't import back into `@agentproto/vscode`). */\nexport const APP_UI_BUILDING_STATUS_ATTR = 'data-agentproto-ui-status=\"building\"'\n\nconst REFRESH_SECONDS = 3\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n}\n\nfunction pageShell(opts: {\n statusAttr?: string\n refresh?: boolean\n title: string\n body: string\n script?: string\n}): string {\n return `<!DOCTYPE html>\n<html lang=\"en\"${opts.statusAttr ? ` ${opts.statusAttr}` : \"\"}>\n<head>\n<meta charset=\"UTF-8\">\n${opts.refresh ? `<meta http-equiv=\"refresh\" content=\"${REFRESH_SECONDS}\">` : \"\"}\n<title>${escapeHtml(opts.title)}</title>\n<style>\n :root { color-scheme: light dark; }\n html, body { height: 100%; }\n body {\n margin: 0; padding: 2rem; box-sizing: border-box;\n display: flex; flex-direction: column; align-items: center; justify-content: center;\n font: 14px -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n color: #ccc; background: #1e1e1e; text-align: center;\n }\n h1 { font-size: 1rem; font-weight: 600; margin: 0 0 0.5rem; }\n p { margin: 0.25rem 0; color: #999; max-width: 34rem; }\n .spinner {\n width: 22px; height: 22px; margin-bottom: 1rem;\n border: 2px solid #444; border-top-color: #8ab4f8; border-radius: 50%;\n animation: agentproto-spin 0.8s linear infinite;\n }\n @keyframes agentproto-spin { to { transform: rotate(360deg); } }\n pre {\n margin-top: 1rem; padding: 0.75rem; max-width: 42rem; width: 90%;\n max-height: 12rem; overflow: auto; text-align: left; white-space: pre-wrap;\n word-break: break-word; background: #111; border: 1px solid #333;\n border-radius: 4px; font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; color: #d99;\n }\n code { color: #ccc; }\n</style>\n</head>\n<body>\n${opts.body}\n${opts.script ? `<script>${opts.script}</script>` : \"\"}\n</body>\n</html>`\n}\n\nexport interface AppUiBuildingHtmlInput {\n readonly appName: string\n /** `Date.now()`-style epoch ms the build actually started — the elapsed\n * counter is computed client-side off this, not baked in as static text,\n * so it keeps ticking between reloads. */\n readonly startedAt: number\n /** Last lines of `.agentproto/ui-build.log`, if cheap to read. */\n readonly logTail?: string\n}\n\n/** The \"still building\" placeholder — see the module doc for the reload\n * strategy. Never blocks on the build itself; the caller decides when to\n * show this (app-ui-build.ts's `resolveAppUiBuildState`). */\nexport function renderAppUiBuildingHtml(input: AppUiBuildingHtmlInput): string {\n const body = `\n <div class=\"spinner\" role=\"status\" aria-label=\"Building\"></div>\n <h1>Building ${escapeHtml(input.appName)} UI&hellip;</h1>\n <p id=\"agentproto-elapsed\">elapsed 0s</p>\n ${input.logTail ? `<pre>${escapeHtml(input.logTail)}</pre>` : \"\"}`\n const script = `(function () {\n var startedAt = ${JSON.stringify(input.startedAt)};\n var el = document.getElementById(\"agentproto-elapsed\");\n function tick() {\n if (!el) return;\n var secs = Math.max(0, Math.round((Date.now() - startedAt) / 1000));\n el.textContent = \"elapsed \" + secs + \"s\";\n }\n tick();\n setInterval(tick, 1000);\n})();`\n return pageShell({\n statusAttr: APP_UI_BUILDING_STATUS_ATTR,\n refresh: true,\n title: `Building ${input.appName}…`,\n body,\n script,\n })\n}\n\nexport interface AppUiErrorHtmlInput {\n readonly appName: string\n readonly message: string\n /** A path worth showing verbatim (the missing file, the removed app dir,\n * the build log). */\n readonly detailPath?: string\n readonly logTail?: string\n /** Defaults to the generic reinstall/APP.md pointer. */\n readonly fix?: string\n}\n\n/** A readable terminal-failure page — never auto-reloads (unlike the\n * building placeholder): the failure already happened, and refreshing a\n * failed `ui.build` run on a timer would just re-run it forever. A human\n * reloading the panel/tab gets a fresh attempt instead. */\nexport function renderAppUiErrorHtml(input: AppUiErrorHtmlInput): string {\n const fix =\n input.fix ??\n \"Reinstall the app with `app_install <dir>`, or check its APP.md `ui` block.\"\n const body = `\n <h1>${escapeHtml(input.appName)} UI is unavailable</h1>\n <p>${escapeHtml(input.message)}</p>\n ${input.detailPath ? `<p><code>${escapeHtml(input.detailPath)}</code></p>` : \"\"}\n <p>${escapeHtml(fix)}</p>\n ${input.logTail ? `<pre>${escapeHtml(input.logTail)}</pre>` : \"\"}`\n return pageShell({ title: `${input.appName}: UI unavailable`, body })\n}\n"]}
1
+ {"version":3,"sources":["../src/app-ui-placeholder.ts"],"names":[],"mappings":";;;;;;AA2CO,IAAM,2BAAA,GAA8B;AAE3C,IAAM,eAAA,GAAkB,CAAA;AAExB,SAAS,WAAW,IAAA,EAAsB;AACxC,EAAA,OAAO,IAAA,CACJ,OAAA,CAAQ,IAAA,EAAM,OAAO,EACrB,OAAA,CAAQ,IAAA,EAAM,MAAM,CAAA,CACpB,QAAQ,IAAA,EAAM,MAAM,CAAA,CACpB,OAAA,CAAQ,MAAM,QAAQ,CAAA;AAC3B;AAEA,SAAS,UAAU,IAAA,EAMR;AACT,EAAA,OAAO,CAAA;AAAA,eAAA,EACQ,KAAK,UAAA,GAAa,CAAA,CAAA,EAAI,IAAA,CAAK,UAAU,KAAK,EAAE,CAAA;AAAA;AAAA;AAAA,EAG3D,IAAA,CAAK,OAAA,GAAU,CAAA,oCAAA,EAAuC,eAAe,OAAO,EAAE;AAAA,OAAA,EACvE,UAAA,CAAW,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4B7B,KAAK,IAAI;AAAA,EACT,KAAK,MAAA,GAAS,CAAA,QAAA,EAAW,IAAA,CAAK,MAAM,cAAc,EAAE;AAAA;AAAA,OAAA,CAAA;AAGtD;AAeO,SAAS,wBAAwB,KAAA,EAAuC;AAC7E,EAAA,MAAM,IAAA,GAAO;AAAA;AAAA,eAAA,EAEE,UAAA,CAAW,KAAA,CAAM,OAAO,CAAC,CAAA;AAAA;AAAA,EAAA,EAEtC,KAAA,CAAM,UAAU,CAAA,KAAA,EAAQ,UAAA,CAAW,MAAM,OAAO,CAAC,WAAW,EAAE,CAAA,CAAA;AAChE,EAAA,MAAM,MAAA,GAAS,CAAA;AAAA,kBAAA,EACG,IAAA,CAAK,SAAA,CAAU,KAAA,CAAM,SAAS,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAAA,CAAA;AAUjD,EAAA,OAAO,SAAA,CAAU;AAAA,IACf,UAAA,EAAY,2BAAA;AAAA,IACZ,OAAA,EAAS,IAAA;AAAA,IACT,KAAA,EAAO,CAAA,SAAA,EAAY,KAAA,CAAM,OAAO,CAAA,MAAA,CAAA;AAAA,IAChC,IAAA;AAAA,IACA;AAAA,GACD,CAAA;AACH;AAiBO,SAAS,qBAAqB,KAAA,EAAoC;AACvE,EAAA,MAAM,GAAA,GACJ,MAAM,GAAA,IACN,6EAAA;AACF,EAAA,MAAM,IAAA,GAAO;AAAA,MAAA,EACP,UAAA,CAAW,KAAA,CAAM,OAAO,CAAC,CAAA;AAAA,KAAA,EAC1B,UAAA,CAAW,KAAA,CAAM,OAAO,CAAC,CAAA;AAAA,EAAA,EAC5B,KAAA,CAAM,aAAa,CAAA,SAAA,EAAY,UAAA,CAAW,MAAM,UAAU,CAAC,gBAAgB,EAAE;AAAA,KAAA,EAC1E,UAAA,CAAW,GAAG,CAAC,CAAA;AAAA,EAAA,EAClB,KAAA,CAAM,UAAU,CAAA,KAAA,EAAQ,UAAA,CAAW,MAAM,OAAO,CAAC,WAAW,EAAE,CAAA,CAAA;AAChE,EAAA,OAAO,SAAA,CAAU,EAAE,KAAA,EAAO,CAAA,EAAG,MAAM,OAAO,CAAA,gBAAA,CAAA,EAAoB,MAAM,CAAA;AACtE","file":"app-ui-placeholder.mjs","sourcesContent":["/**\n * Self-contained HTML pages served in place of an installed app's real UI\n * bundle while it's building, or when it can never be served (missing file\n * with no `ui.build`, a failed build, a removed app dir). Both surfaces that\n * serve an app's UI as a PAGE — the `ui://app_ui_<id>/view` MCP resource\n * (app-ui-apps.ts) and `GET /apps/:appId/ui` (http-server.ts) — render\n * through here instead of ever putting raw `{\"error\":...}` JSON in a page\n * body. That JSON shape is kept exactly as-is for MCP TOOL callers (e.g.\n * `app_install`'s error result) — this module is for the two PAGE routes\n * only.\n *\n * Reload strategy — deliberately two different mechanisms for the two\n * hosts, because neither works everywhere:\n *\n * - The VS Code webview panel (packages/vscode appPanel.ts) embeds this\n * page as an inner `srcdoc` iframe of an outer document it fully\n * controls (buildAppHostHtml). `srcdoc` has no URL, so a navigation\n * inside it (meta-refresh) just re-parses the SAME string — it can\n * never pick up new content on its own. The extension host is what can:\n * appPanel.ts detects `BUILDING_STATUS_ATTR` on the returned html and\n * re-issues `resources/read` on a timer, replacing `panel.webview.html`\n * wholesale until the marker is gone (see appPanel.logic.ts's\n * `isAppUiBuilding`).\n * - `GET /apps/:appId/ui` (http-server.ts) IS a real top-level navigation\n * target, so this page's own `<meta http-equiv=\"refresh\">` re-requests\n * the same URL from the server directly — no host cooperation needed.\n *\n * Both mechanisms are outside this module's own script, which is\n * deliberate: the page also has to survive the strictest CSP either host\n * imposes — the VS Code outer document sets `default-src 'none'; script-src\n * 'unsafe-inline'; style-src 'unsafe-inline'` with NO `connect-src`, and the\n * srcdoc iframe inherits it verbatim, so nothing here may `fetch`, open a\n * `WebSocket`/`EventSource`, or load an external image/font. The one bit of\n * live JS this page does carry — the elapsed-time counter — only touches\n * `Date.now()` and the DOM off a baked-in timestamp, neither of which needs\n * network access.\n */\n\n/** `data-agentproto-ui-status` value while a build is in flight (or about to\n * start) — `appPanel.logic.ts`'s `isAppUiBuilding` matches on this exact\n * attribute/value pair; keep the two in lockstep (same cross-package\n * duplication the file's own `appUiToolId` already accepts, since a\n * `ui://`-served page can't import back into `@agentproto/vscode`). */\nexport const APP_UI_BUILDING_STATUS_ATTR = 'data-agentproto-ui-status=\"building\"'\n\nconst REFRESH_SECONDS = 3\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n}\n\nfunction pageShell(opts: {\n statusAttr?: string\n refresh?: boolean\n title: string\n body: string\n script?: string\n}): string {\n return `<!DOCTYPE html>\n<html lang=\"en\"${opts.statusAttr ? ` ${opts.statusAttr}` : \"\"}>\n<head>\n<meta charset=\"UTF-8\">\n${opts.refresh ? `<meta http-equiv=\"refresh\" content=\"${REFRESH_SECONDS}\">` : \"\"}\n<title>${escapeHtml(opts.title)}</title>\n<style>\n :root { color-scheme: light dark; }\n html, body { height: 100%; }\n body {\n margin: 0; padding: 2rem; box-sizing: border-box;\n display: flex; flex-direction: column; align-items: center; justify-content: center;\n font: 14px -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n color: #ccc; background: #1e1e1e; text-align: center;\n }\n h1 { font-size: 1rem; font-weight: 600; margin: 0 0 0.5rem; }\n p { margin: 0.25rem 0; color: #999; max-width: 34rem; }\n .spinner {\n width: 22px; height: 22px; margin-bottom: 1rem;\n border: 2px solid #444; border-top-color: #8ab4f8; border-radius: 50%;\n animation: agentproto-spin 0.8s linear infinite;\n }\n @keyframes agentproto-spin { to { transform: rotate(360deg); } }\n pre {\n margin-top: 1rem; padding: 0.75rem; max-width: 42rem; width: 90%;\n max-height: 12rem; overflow: auto; text-align: left; white-space: pre-wrap;\n word-break: break-word; background: #111; border: 1px solid #333;\n border-radius: 4px; font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; color: #d99;\n }\n code { color: #ccc; }\n</style>\n</head>\n<body>\n${opts.body}\n${opts.script ? `<script>${opts.script}</script>` : \"\"}\n</body>\n</html>`\n}\n\nexport interface AppUiBuildingHtmlInput {\n readonly appName: string\n /** `Date.now()`-style epoch ms the build actually started — the elapsed\n * counter is computed client-side off this, not baked in as static text,\n * so it keeps ticking between reloads. */\n readonly startedAt: number\n /** Last lines of the app's ui-build log (`appUiBuildLogPath`), if cheap to read. */\n readonly logTail?: string\n}\n\n/** The \"still building\" placeholder — see the module doc for the reload\n * strategy. Never blocks on the build itself; the caller decides when to\n * show this (app-ui-build.ts's `resolveAppUiBuildState`). */\nexport function renderAppUiBuildingHtml(input: AppUiBuildingHtmlInput): string {\n const body = `\n <div class=\"spinner\" role=\"status\" aria-label=\"Building\"></div>\n <h1>Building ${escapeHtml(input.appName)} UI&hellip;</h1>\n <p id=\"agentproto-elapsed\">elapsed 0s</p>\n ${input.logTail ? `<pre>${escapeHtml(input.logTail)}</pre>` : \"\"}`\n const script = `(function () {\n var startedAt = ${JSON.stringify(input.startedAt)};\n var el = document.getElementById(\"agentproto-elapsed\");\n function tick() {\n if (!el) return;\n var secs = Math.max(0, Math.round((Date.now() - startedAt) / 1000));\n el.textContent = \"elapsed \" + secs + \"s\";\n }\n tick();\n setInterval(tick, 1000);\n})();`\n return pageShell({\n statusAttr: APP_UI_BUILDING_STATUS_ATTR,\n refresh: true,\n title: `Building ${input.appName}…`,\n body,\n script,\n })\n}\n\nexport interface AppUiErrorHtmlInput {\n readonly appName: string\n readonly message: string\n /** A path worth showing verbatim (the missing file, the removed app dir,\n * the build log). */\n readonly detailPath?: string\n readonly logTail?: string\n /** Defaults to the generic reinstall/APP.md pointer. */\n readonly fix?: string\n}\n\n/** A readable terminal-failure page — never auto-reloads (unlike the\n * building placeholder): the failure already happened, and refreshing a\n * failed `ui.build` run on a timer would just re-run it forever. A human\n * reloading the panel/tab gets a fresh attempt instead. */\nexport function renderAppUiErrorHtml(input: AppUiErrorHtmlInput): string {\n const fix =\n input.fix ??\n \"Reinstall the app with `app_install <dir>`, or check its APP.md `ui` block.\"\n const body = `\n <h1>${escapeHtml(input.appName)} UI is unavailable</h1>\n <p>${escapeHtml(input.message)}</p>\n ${input.detailPath ? `<p><code>${escapeHtml(input.detailPath)}</code></p>` : \"\"}\n <p>${escapeHtml(fix)}</p>\n ${input.logTail ? `<pre>${escapeHtml(input.logTail)}</pre>` : \"\"}`\n return pageShell({ title: `${input.appName}: UI unavailable`, body })\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { AuthProfile } from '@agentproto/auth';
2
- import { A as AdapterAuthDescriptor } from './spawn-defaults-Cp9SGZex.js';
3
- import { R as RouteSpec } from './session-config-DxIIuJFY.js';
2
+ import { A as AdapterAuthDescriptor } from './spawn-defaults-Dm7nbxcP.js';
3
+ import { R as RouteSpec } from './session-config-BUDM7T1B.js';
4
4
  import '@agentproto/acp';
5
5
  import '@agentproto/command-sandbox';
6
6
  import '@agentproto/model-catalog';
@@ -27,6 +27,10 @@ var providers_store_exports = {};
27
27
  __reExport(providers_store_exports, providers_store_star);
28
28
 
29
29
  // src/spawn-defaults.ts
30
+ function modelIdPrefixProvider(modelId) {
31
+ const slash = modelId.indexOf("/");
32
+ return slash > 0 ? modelId.slice(0, slash) : void 0;
33
+ }
30
34
  function subscriptionSurfaceFor(sub, endpoint) {
31
35
  if (sub === void 0) return void 0;
32
36
  if (!Array.isArray(sub)) {
@@ -34,6 +38,31 @@ function subscriptionSurfaceFor(sub, endpoint) {
34
38
  }
35
39
  return sub.find((s) => s.provider !== void 0 && s.provider === endpoint) ?? sub.find((s) => s.provider === void 0);
36
40
  }
41
+ function directAuthMethods(descriptor, endpoint) {
42
+ const methods = [];
43
+ if (subscriptionSurfaceFor(descriptor?.authSubscription, endpoint) !== void 0) {
44
+ methods.push("oauth-bearer");
45
+ }
46
+ if (descriptor?.provider || descriptor?.modelDerivedApiKey) methods.push("api-key");
47
+ return methods;
48
+ }
49
+ function spawnEligibilityManifest(adapter, descriptor, route, model) {
50
+ const directEndpoint = descriptor?.provider ?? (model ? descriptor?.modelProviders?.[model] : void 0) ?? (model && descriptor?.modelDerivedApiKey ? modelIdPrefixProvider(model) : void 0) ?? (model ? getModelProvider(model) : void 0);
51
+ const routeId = route?.gateway ?? directEndpoint;
52
+ if (!routeId) return void 0;
53
+ const direct = directEndpoint !== void 0 && routeId === directEndpoint;
54
+ return {
55
+ manifest: {
56
+ id: adapter,
57
+ endpointByRoute: { [routeId]: direct ? directEndpoint : routeId },
58
+ methodsByRoute: {
59
+ [routeId]: direct ? directAuthMethods(descriptor, directEndpoint) : ["api-key"]
60
+ }
61
+ },
62
+ routeId,
63
+ direct
64
+ };
65
+ }
37
66
 
38
67
  // src/catalog-models.ts
39
68
  var WIDENING_ROUTES = [
@@ -270,6 +299,7 @@ function buildCatalogModels(input) {
270
299
  ];
271
300
  const merged = mergeContributions(contributions);
272
301
  const query = input.query ?? {};
302
+ const queryAdapter = query.adapter ? input.adapters.find((a) => a.slug === query.adapter) : void 0;
273
303
  const servableByRoute = /* @__PURE__ */ new Map();
274
304
  for (const row of merged) {
275
305
  const set = servableByRoute.get(row.route) ?? /* @__PURE__ */ new Set();
@@ -284,12 +314,18 @@ function buildCatalogModels(input) {
284
314
  if (query.adapter && !row.adapters.includes(query.adapter)) continue;
285
315
  const isDirectVendorRoute = row.route === row.vendor && row.adapterModes.length === 0;
286
316
  const walletEligible = !isDirectVendorRoute || checkModelWalletEligibility(`${row.vendor}/${row.product}`, row.route).ok;
287
- const manifest = {
317
+ const projected = queryAdapter === void 0 ? void 0 : queryAdapter.authDescriptor ? spawnEligibilityManifest(
318
+ queryAdapter.slug,
319
+ queryAdapter.authDescriptor,
320
+ { gateway: row.route },
321
+ row.ref
322
+ ) : null;
323
+ const manifest = projected?.manifest ?? {
288
324
  id: `${row.vendor}/${row.product}@${row.route}`,
289
325
  endpointByRoute: { [row.route]: billedVendor(row.vendor, row.route) },
290
326
  methodsByRoute: { [row.route]: row.methods }
291
327
  };
292
- const eligible = walletEligible ? eligibleProfiles(input.profiles, manifest, row.route).filter(
328
+ const eligible = walletEligible && projected !== null ? eligibleProfiles(input.profiles, manifest, projected?.routeId ?? row.route).filter(
293
329
  (p) => profileAllowsModel(p, row.ref, `${row.vendor}/${row.product}`)
294
330
  ) : [];
295
331
  const runnable = eligible.length > 0;