Vendored @cordisjs/plugin-hmr uses decorators — non-erasable syntax the
--experimental-transform-types demo:tui launch refuses — so its row made the
shipped TUI composition unbootable from source while the tsx-launched
snapshot harness masked it. The stale !!js head comment goes with it;
restoring dev HMR is tracked separately.
New credentials data-structure page (type-equiv manifested), group README,
rewritten llm-deepseek/llm-pi-ai READMEs (dynamic configuration, dict
profiles, credential chain), capability-seams/service-role registration,
Agent Note (bilingual), demo compositions mounting settings-local +
credentials-local with no inline key plumbing, installSettingsSection
consumer helper on the settings seam (deduplicating both adapters' wiring),
jscpd symmetry markers for the provider twins, runtime-closure additions for
python/sdk-runtime, and doc-budget ceilings AGENTS.md 1750→1755 /
packages/README.md 850→865 for the structural one-line group rows.
The pending question now owns exactly two surfaces: the redesigned
QuestionComposer takeover (footer pager, checkbox multi-select,
always-visible custom input, locale-injected bilingual chrome) collects
the answers, and a dedicated ask_user_question toolview row reports the
interaction outcome — waiting, N/M answered, cancelled (ASK_CANCELLED),
or interrupted with stopped semantics (ASK_ABORTED). PendingCard narrows
to approval waits only. Toolview leading icons and the hover chevron
unify on the tertiary label color, the checklist glyph matches the
14px figma extract, and dev-watch registers CSS modules so css-only
edits rebuild.
llm-deepseek gains a Loader+Include composition spec proving external
settings.yaml/.env edits reach the very next request, and a real-API e2e
where only a credentials-local document holds the key. The headless example
pins the first-run missing-credential UX as a keyless stream-json snapshot
(new credentials.cordis.snapshot.yml scenario); runLoaderSmoke learns
expectedExitCode so a designed failure surface can be pinned instead of
masked.
providers becomes a dict keyed by provider route, so the composition base
and the llm-pi-ai settings section merge per provider and the route set is
structural; the pre-release array shape and per-profile provider field fail
loud with migration directions. The adapter reads a profiles thunk once per
operation and resolves the credential per stream call (literal apiKey, then
apiKeyEnv through ctx.credentials with an ambient env fallback, then pi-ai's
provider-native discovery), so key, endpoint, and knob changes reach the
next request without restarts. Route-set or captured-retry-policy changes
re-register the same adapter instance in one synchronous section; an invalid
settings snapshot keeps the last good profiles.
The adapter takes an options thunk and a per-stream credential resolver
instead of frozen construction facts: base URL, catalog, defaults, idle
budget, and the API key re-resolve at each operation, so a settings or
credential change reaches the very next request while in-flight streams
keep the facts they started with. resolveAdapterOptions is the one explicit
resolve step (entry config fails loud at load; a live snapshot failing a
beyond-schema bound keeps the last good options). The plugin layers its
entry config under the optional llm-deepseek settings section and resolves
keys literal-first through ctx.credentials with an ambient env fallback; a
missing key now registers the route, warns, and fails each request with
actionable MISSING_CREDENTIAL instead of failing plugin load. The
registration-captured retry policy re-registers the route in place when it
changes.
Live environment wins read-only (shadowed writes reject instead of
appearing to succeed); the file is the writable source with byte-preserving
line edits, a quoting ladder dotenv reads back verbatim, atomic 0600
writes, wholesale snapshot replacement on reload, and write-drain
teardown.
Node 26.0.0 removed --experimental-transform-types, so the native
source-launch chain cannot start anywhere on that line, and strip-only
mode rejects the vendored syntax (parameter properties, decorators,
runtime enums/namespaces). Switch bin/dsh, the root dsh/demo:tui/
demo:web scripts, and the Code Mode TUI overlay to node --import
tsx/esm: one launch vector across the whole engines range, ~0.4s faster
than the full tsx default (the CJS hook stays off; the graph is
ESM-only).
Delete scripts/tspath-loader.ts and apps/cli/src/tsconfig-paths-loader.ts:
tsx owns both transformation and tsconfig paths projection. Add
dsh-source-launch-smoke to the node-compat gates so the 22.19/26 matrix
executes the real launch vector; no CI job did, which is how the Node 26
breakage shipped silently.
Supersedes the native-TypeScript-source-launch Agent Note (new note
records the profiling evidence and rejected alternatives).
Both packages import @deepseek-ai/dsh-llm at runtime (createUserMessage)
but declared it only in devDependencies; any resolver that honors
declared runtime dependencies resolves the import to a stale or missing
artifact. Found by the (since removed) source-launch declared-dependency
check.
Four review findings. Two are defects the previous two rounds introduced,
which the existing tests did not catch:
A backspace erased raw bytes, so one landing after an SGR reset ate part of
the escape: `\x1b[31mabc\x1b[0m\b\bXY` left `\x1b[` and repainted the rest of
the line with whatever the remainder parsed as. Backspaces now resolve over
VISIBLE characters — a CSI sequence is one indivisible unit a backspace steps
over on its way to the last printed character, so the surviving text keeps
the color its run authored.
The cwd normalizer popped a UNC share root: `\\server\share` with a `..`
became `/server`, losing the separators too. A UNC path's server and share
are its root, and Windows cannot climb above a share, so they are split off
and the remainder collapses against that root.
The other two are gaps the earlier fixes left:
The render-site fallback row still passed the args-derived summary, so any
terminal-declaring tool without its own keyed row (`terminal_send`) lost the
contract's above-card description. It now prefers the description exactly as
BashRow does.
A settled call read `call?.cwd`, which cannot tell "the call omitted a cwd"
from "the paging window dropped the call head". The second case has no cwd
anywhere and the original call may have used an explicit workdir, so it now
draws a bare `$` instead of naming the session workspace.
Clearing waitingApprovals in handleConnected raced the reconnect replay:
mux frames flow from stream open while onConnected waits for the
readiness handshake, so a replayed approval/requested could land first
and be wiped — amber dot and answerable card lost until the next
generation. The sweep moves to generation death (onStateChange
'reconnecting'), before any next-generation frame can exist, and now
also drops buffered answerable frames (approval/question pairs) whose
dead-generation rpcIds could never be answered — a session instantiated
later no longer replays zombie takeover cards. session/queued buffering
already re-baselines per generation; this closes the same window for
the interaction frames.
Two races from the #572 review, still live in the ported registry:
An ask whose signal aborted between the service's own check and the
microtask-deferred waterfall dispatch would register its abort listener
AFTER the signal fired — never invoked, entry pending forever, zombie
frame on every mux replay. The answerer now settles 'cancelled'
synchronously before publishing anything.
The audit back-scan let a callId-less ask claim the newest unclaimed
asked record even when that record carried another call's id. Pairing is
now shape-symmetric: callId-bearing asks take exactly their call's
record, callId-less asks take only callId-less records — neither can
steal under parallel asks.
writeFileAtomic: exclusive-create random-suffix temp + rename carrying the
caller-stated mode; settings-local persistSection now consumes it. The
credentials-local store shares it next.
Nothing under examples/ references @deepseek-ai/dsh-bash now that the
tmux-context fixtures are gone; the examples workspace ignores every
@deepseek-ai/* dependency in knip, so no gate could catch it. This leaves
examples/package.json untouched by the PR.
Disposability parity with the question provider: a gateway disposed while
approvals are pending settles every registry entry as 'cancelled' (the
service's fail-closed vocabulary), so no ctx.approval ask dangles past the
proxy's lifetime and mux subscribers see the withdrawal. Spec mounts the
proxy on its own fiber and drives dispose with a live ask.
Addresses the ds-review-bot suggestion on PR #851.
A multi-line command repeated the cwd label on every prompt row, which
states something the view does not know: it carries ONE working directory —
where the call started — and a `cd` in the command moves later lines
elsewhere. `cd ~` then `ls` rendered both rows labelled with the session
workspace while `ls` actually listed the home directory.
The label now appears on the first row only, and later rows keep a bare `$`
so they still read as prompts. Same reasoning as the run-state dot: neither
a per-line directory nor a per-line exit status exists to report.
The built-bundle snapshot records the effect on fixture turn 60's two-line
command (`fixture echo done` becomes `$ echo done`).
A popup on a host command is not a second command — it is what that
command's BARE invocation does on this client. CommandContribution loses
hostBacked (contributions are pure client commands again; a host-name
collision fails loud, unchanged for /model), and the contract gains
CommandDecoration + command.decorate(): key = the HOST command name, no
catalog row, no claim participation. Dispatch consults decorations only on
the bare paths (menu pick / bare enter) after the host row resolves; space
and argued enter never see them — the two edges hostBacked had to guard
explicitly hold by construction in the decoration model. A decorated name
with no host row in the session's directory never fires (a decoration
cannot manufacture a command).
ui-permission switches register→decorate with zero behavior change
(options still read the permissions projection; a pick still submits
'/permission <preset>'). Specs rewrite to the decoration semantics: no
catalog row, bare-enter popup vs argued-enter host claim, space host
claim, no-host-row miss, unavailable fall-through, duplicate fail-loud.
Three review findings, each verified against the presentation contract:
The call view's `description` was dropped, so a presenter that authors one
(`terminal_send` declares `Terminal <id>`) lost the contract's above-card text
and the row fell back to an unrelated args-derived summary. It now rides the
same derivation and outranks that summary.
A relative workdir was concatenated but never normalized, while the bash
executor resolves it before running: with session cwd `/w/app` and workdir
`..` the command runs in `/w`, yet the card displayed the label `..`. The
resolved path now collapses `.`/`..` segments, drops a `..` that would climb
past a root the way a filesystem does, and keeps a Windows path's separators
since the value is only ever displayed.
`run_code` sub-dispatches carry no presenter views on the shipped wire —
`session.ts` folds `tool/code-dispatch(-start)` with null views and the host's
`viewFor` presents only top-level call/result events — so a nested bash call
cannot reach a terminal card. The existing test only passed by injecting views
that path cannot produce; it now says so, and a second arm pins the no-view
shape the wire actually delivers.
Restoring master's fixture also fixed the todo snapshot lane, which my earlier
merge had broken by dropping the projection support the todo dock reads. The
terminal sample turn moved ahead of the todo turn, because the standing plan
retires at the next `turn/start` and a turn appended after it emptied the dock.
The card props are now nested under `card` so a render site spreads exactly the
primitive's own surface, and the fixture reads each sample's authored exit
status instead of re-implementing the bash tool's `parseExitStatus`.
Selecting a preset from the hero pushed the session into the conversation
view: the /permission switch logs its command/run + command/done pair, the
pair folds into flow nodes, and the composerPhase predicate counted ANY
node as conversation — so the hero (composerPhase === 'blank') collapsed.
The host-side blank bit was already correct (sessionBlank = no turn/start;
knob events open no turn), but the client derives its phase from window
content, and command rows are log-only records, not conversation.
derivePhase's hasContent now excludes command nodes — the client mirror of
the host predicate. The knob events themselves never fold (not
surface-eligible), so the pair was the only leak. Covers /plan on the hero
identically (same lifecycle pair, same predicate).
Specs: the host blank spec pins the three knob events as standalone
events; a session spec drives the /permission pair through the live path
and asserts phase stays 'blank' while the command node renders.