feat(web): author agent presets from a settings page
A composition is a file, but "edit it on the filesystem" is not a browser affordance. The roster gains `read`/`write`/`remove` beside `select`, and the browser gains a settings section over them: the presets as rows, one composition open in a YAML editor at a time, and per-row default, duplicate, and delete. All four authoring methods are loopback-pinned. A composition names the plugins a session runs, so reading one is reconnaissance, writing one is arbitrary capability, and selecting one can move a session onto a preset that edits the live runtime. `agentPreset.list` deliberately stays ordinary and now reports `authorable`, so a surface knows whether creating is possible at all rather than offering a button whose save always fails. Authoring starts by duplicating: a shipped preset opens read-only because the deployment's copy is what a broken local one is compared against. Ids are contained before they become directory names, and the text is parsed with the loader's own schema, so a save cannot leave a file no session could load. Fixes a defect the real-composition test found: a preset written under the user's home could never mount, because the loader resolves a row against the composition's own directory and Node's `node_modules` walk from there never reaches the installed harness. The mount now records the host base and sends bare specifiers there, leaving relative paths resolving from the preset. Also closes the coverage the earlier surfaces in this stack shipped without — the General row, the composer seat, and the plugin halves now have tests.
This commit is contained in:
@@ -41,6 +41,14 @@ interface MountedTree {
|
||||
*/
|
||||
const mounted = new WeakMap<object, MountedTree>()
|
||||
|
||||
/**
|
||||
* The base URL bare specifiers resolve against, per pending mount, keyed by the
|
||||
* same config object. Recorded before the subtree is plugged, because `Include`
|
||||
* rewrites its own context's `baseUrl` to the composition's directory and the
|
||||
* pre-mount value is the only handle on where the harness itself lives.
|
||||
*/
|
||||
const harnessBase = new WeakMap<object, string>()
|
||||
|
||||
/**
|
||||
* Include subclass that publishes its tree and fiber for the audit, and never
|
||||
* writes to the file it read.
|
||||
@@ -51,6 +59,33 @@ class PresetTree extends Include {
|
||||
mounted.set(config, { tree: this, fiber: ctx.fiber })
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a bare specifier from the harness rather than from the preset.
|
||||
*
|
||||
* `EntryTree.import()` resolves against the tree's own `baseUrl`, which
|
||||
* `Include` sets to the composition's directory. That is right for a
|
||||
* relative specifier — a preset's own files travel with it — and wrong for
|
||||
* a package name: a locally authored preset lives under the user's home,
|
||||
* where Node's upward `node_modules` walk never reaches the harness's own
|
||||
* dependencies, so every `@deepseek-ai/dsh-*` row would fail to import. The
|
||||
* mount records the host composition's base instead, which is inside the
|
||||
* installed harness, and bare names resolve from there.
|
||||
* @param name - the module specifier from the row.
|
||||
* @param getOuterStack - the loader's stack composer for import diagnostics.
|
||||
* @returns the imported module, or the `cordis:` builtin.
|
||||
*/
|
||||
override import(name: string, getOuterStack?: () => string[]): unknown {
|
||||
const base = harnessBase.get(this.config)
|
||||
/* v8 ignore next -- every PresetTree is constructed by `mountPreset`, which records the base first */
|
||||
if (base === undefined) return super.import(name, getOuterStack)
|
||||
if (name.startsWith('.') || name.startsWith('cordis:')) return super.import(name, getOuterStack)
|
||||
const internal = this.ctx.loader.internal
|
||||
/* v8 ignore next -- Node always supplies the internal module loader; the branch keeps a
|
||||
hypothetical embedder from losing the row's name in a resolution error. */
|
||||
if (internal === undefined) return super.import(name, getOuterStack)
|
||||
return internal.import(name, base, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* A preset is an input, never a persistence target.
|
||||
*
|
||||
@@ -269,6 +304,11 @@ export async function mountPreset(agentCtx: Context, preset: AgentPreset): Promi
|
||||
)
|
||||
}
|
||||
const config: Include.Config = { path: pathToFileURL(preset.path).href }
|
||||
// Captured before the subtree exists: the agent context still carries the
|
||||
// host composition's base, which is inside the installed harness and is
|
||||
// therefore where a row's package name has to resolve from.
|
||||
/* v8 ignore next -- the Loader sets `baseUrl` on the root before any agent context derives from it */
|
||||
if (agentCtx.baseUrl !== undefined) harnessBase.set(config, agentCtx.baseUrl)
|
||||
// Before the record this mount is about to add: every session takes this
|
||||
// path, so it is what keeps the set bounded on a host that never reads it.
|
||||
pruneDisposedMounts()
|
||||
|
||||
Reference in New Issue
Block a user