Master stabilized two of the three hand-built markdown fixtures by spacing
their event times, so the stats line's LLM segment stops depending on how fast
the replay ran. `markdown-images` was left on the old shape and kept failing
for exactly that reason — a different test each run, always the same one-line
difference.
Pin it the same way and record the segment its golden now always shows. This
supersedes the normalizer that dropped those segments outright, reverted here:
pinning the fixture keeps a real part of the page in the goldens instead of
hiding it from every one of them, and master's two goldens already record it.
`StatsLine` renders its LLM, tool-call, and throughput segments only while the
matching measurement exceeds zero, and all three are wall clock taken during
the replay. A machine that finishes a step inside one millisecond drops the
segment a slower one keeps, so a golden recorded what the recording machine's
speed was rather than what the page shows. Goldens across this suite already
disagreed about the LLM segment for that reason, and CI failed on whichever
test landed on a slow enough runner — a different test each run, always the
same one-line difference.
Tokenizing the values was never enough, because presence is what moves. The
normalizer now drops those segments outright, each taking one adjacent
separator so nothing is left holding a dangling separator or a doubled space,
and the recorded goldens are normalized the same way. `TTFT avg` stays: it
gates on a step count the fixture determines.
The Models page could name a provider's key and little else. Adding an
OpenAI-compatible gateway meant opening $DSH_HOME/settings.yaml and
knowing the profile shape; correcting a stale context window meant the
same. This layer puts both on the page: a card that declares a route
pi-ai does not ship — id, endpoint, protocol, key, models — and a model
list on the pi-ai editor that can ask the provider what it serves and
adopt the answer.
It follows the DeepSeek catalog editor that landed in #1050 rather than
inventing a second look for the same job. Both editors now share the
section shell and heading, the danger-tinted delete, the add-model
button, the empty state, the per-row validator that names a bad row by
its position, and one K/M capacity vocabulary — 256K and 1M are read and
spelled back, while settings.yaml still stores plain token counts. The
row type is structurally open like that editor's, so a profile field
this card does not edit survives an edit here.
Three of that editor's decisions replaced weaker ones this branch had
made. Inheritance now reads the composition base rather than the
effective value, which would echo an override back the moment a reset
dropped it. Validation names the offending row instead of stating a
blanket problem. And emptying the list is no longer conflated with
handing the catalog back to the adapter — those are separate acts, with
separate affordances.
The create write carries the revision the card opened at, so a route
another tab declared meanwhile is a conflict rather than a silent
overwrite of its profile.
The CJK-strong and inline-code-link goldens predate the flanking-space
footer separators and drifted on the master merge; re-record them with
the accessible space, matching every other golden.
planSummary treated whitespace-only content as nameable, so a rejected call
whose args survive verbatim rendered a blank active clause beside a live +N.
The tool's own rule is trimmed non-empty; the row now uses it.
Also hoists the duplicated DSH_SNAPSHOT refresh flag out of the two assembled
snapshot files into their shared assembled-boot scaffolding.
ea2fc29530 gave the assistant footer separators flanking spaces, so the
accessible text reads `{{clock}} Ran for {{duration}}`. The CJK-strong and
inline-code-link goldens were recorded on a base that predates it and merged
without a re-record; master's push runs skip the snapshot lane, so nothing
caught the drift until a branch merged both. Every other golden already
carries the space.
The plan strip header has not emitted `<done>/<total> tasks · <n> in progress`
since #740 replaced it with `·`-joined per-status counts. Four sites still
quoted the old string: plan-summary.ts's module JSDoc, ui-conversation's
README pair, and the web-todo-display note pair.
The parallel-in-progress note's regression example mixed a four-item plan with
an eight-item row reading, and built-boot.snapshot.ts still called itself the
only test loading the built bundles.
The `+N` active count rides ToolRow's non-shrinking `summarySuffix` slot,
but only jsdom package suites covered it: the ACP snapshots render no web
surface and the built-boot smoke asserts no todo row. Add
`apps/web/tests/todo-row.snapshot.ts`, which boots the built client bundles
against the keyless fixture transport and records `summary`, `suffix`, and
the plan strip's header as separate fields, so folding the count back into
the summary string changes the expected output.
The three assembled-jsdom files now share `apps/web/tests/assembled-boot.ts`
instead of each carrying its own copy of the boot entry list, bundle map,
jsdom stubs, and mount call.
Also: name the policy branch in each `allowParallelInProgress` test title so
no case asserting `true` sits under a `false` describe, reword the stale cap
comment in todo-panel.spec.tsx, and record the plan strip's real header
format in the Agent Note (per-status counts, not `<done>/<total> tasks`).
- Relative path specs (., ../plugin, file:/link: forms) anchor to the
invoking directory before forwarding: pnpm's cwd is the profile dir, so a
bare 'add .' from a plugin checkout used to self-link the profile (exit 0,
nothing installed). Bare paths stay bare and prefixed specs keep their
prefix, preserving pnpm's link-vs-copy semantics.
- dsh.plugins reconciles against the INSTALLED state on every successful
pnpm run, not the dependency diff: an update whose new version gains
dsh.patch activates the layer; a version that drops it (or a removal)
deactivates it. Template bundles are never touched.
- A failed pnpm run now names the profile directory, and a git-spec failure
explains pnpm >=10's prepare-script block with a pointer at the profile's
pnpm-workspace.yaml allowBuilds (turtle-ui's prepare-based git install is
the reference consumer); reference README documents all three.
All agent/* and agent-loop/config-start-failed events take one payload
object carrying the agent subject; waterfall/serial payloads require a
signal and keep next as the final argument. PreStepContext and
RequestFailureContext are unfolded into payloads and retired.
goal/changed follows the same shape so agentEvents keeps its listener
error containment. ReactLoopAgent builds its scope carrier once in the
constructor. Regenerates scope resolvers, tool-cordis api catalog, and
docs catalogs; updates all affected listeners, tests, and the
core-data-structures docs (en + zh).
The old $DSH_HOME/config.yaml personal overlay returns under the profile
scheme's filename: machine-local preferences that apply to every profile,
loaded after the profile's own cordis.patch.yml (so the home layer outranks
it) and before --patch overlays and flag patches. Both user layers are
hot-reloaded on long-lived surfaces and shown in --dump-config with their
own provenance labels; the built-bin e2e covers the home layer landing live.