feat(web): choose the agent preset on the new-session screen

The composer seat spent nearly all its life disabled: a session's
composition is fixed once a turn has run. Move the choice to the
new-session screen beside the workspace picker, where it still works,
and let the session header report what a running session runs.

The hero pick is staged rather than applied — that screen precedes the
session it belongs to. It lands when a session becomes current and is
still blank, which covers both the session a workspace connect creates
and the blank one it reuses; riding `sessions.create` would miss the
second. It is spent on first use, matching the workspace picker.

Fix the durability the header field claimed but never had: `agentPreset`
was declared on `SessionHeader` and dropped by the JSONL header line, the
SQLite sessions row, the derived query index, and the cold list
projection, so every resumed session came back composed from nothing.

Add the web e2e lane that would have caught it — the one lane that mounts
the shipped roster, which needed `cordis:group` in the scaffold's Loader
builtins, as `mountRootInclude` already registers.
This commit is contained in:
Yichen Jiang
2026-08-05 18:51:11 +08:00
parent 4bb9836abe
commit 2b8a0a8cff
52 changed files with 1238 additions and 399 deletions

View File

@@ -285,6 +285,25 @@ export function inactiveRows(tree: EntryTree): string[] {
return lines
}
/**
* The reportable text of a mount failure.
*
* The loader reports several failed rows as one `AggregateError`, whose own
* message names none of them; without flattening, a composition that fails on
* two rows says only "loader entries failed to apply" and the operator has
* nothing to act on.
* @param error - the value the mount rejected with.
* @returns a single-line-per-cause description.
*/
function mountDetail(error: unknown): string {
/* v8 ignore next -- every path into the mount's catch throws an Error: the loader
wraps a row's thrown value before it propagates, and this module's own
rejections are Errors. The fallback keeps a hostile value readable. */
if (!(error instanceof Error)) return String(error)
if (!(error instanceof AggregateError)) return error.message
return [error.message, ...error.errors.map(cause => `- ${mountDetail(cause)}`)].join('\n')
}
/**
* Mount `preset` under `agentCtx` and return only once every row is usable.
*
@@ -341,10 +360,6 @@ export async function mountPreset(agentCtx: Context, preset: AgentPreset): Promi
// Swallows only this subtree's teardown failure. The mount error below is
// the actionable one, and the discarded fiber is unreachable either way.
}
/* v8 ignore next -- every path into this catch throws an Error: the loader
wraps a row's thrown value before it propagates, and this module's own
rejections are Errors. The fallback keeps a hostile value readable. */
const detail = error instanceof Error ? error.message : String(error)
throw new PresetMountError(preset.id, `${detail} (${preset.path})`, { cause: error })
throw new PresetMountError(preset.id, `${mountDetail(error)} (${preset.path})`, { cause: error })
}
}