The path editor and the folder-name input carried identical inline IME
handlers — the cross-file clone jscpd flagged against WorkspacePicker; one
component-level guard object serves both.
- Open and New folder disable while a path draft is uncommitted: targetPath
still names the previous selection/listing, and committing against it
while a different path shows in the header adopts the wrong directory.
- The Miller columns keep their own row so a status/error line renders below
them inside the card instead of competing as a third flex item the dialog
clips off-screen.
- Both text inputs (path editor, folder name) carry the IME composition
guard the workspace-name inputs already had: a composing Enter confirms
the candidate, never submits.
Two real branches gain assertions (effectivePermissionPreset's backward
scan over non-preset events; the popup options() throw when the projection
vanished between availability and open — a sync throw, not a rejection).
The empty ui-permission node half joins the ui-model entry on the
client-lane coverage debt list (same pure-UI plugin shape, same TODO).
With no listed level (an unreadable or missing home directory), the
path-edit zone previously disabled forever, stranding the operator on the
alert with only Cancel; it now opens with an empty draft so an absolute
path remains the way forward. Covered by a recovery test.
Two overlong lines wrap (fixture permissionSelectOf signature, apply.ts
type-import list) and three test-side unnecessary assertions drop
(eslint --fix).
gen-cordis-catalog/api, config/persistence catalogs, module graph, and doc
graphs pick up the ui-permission package, the permissions projection key,
and the /permission command; KnobState and PermissionSelect join the
type-link exemptions (owned by the permission package's own docs), and
ui-permission joins the sentence Model Experience allowlist (indirect via
the host command). The ui-conversation and dsh-permission READMEs gain
their zh halves for the approval/chip and projection/command paragraphs;
all three touched pairs re-record.
First consumer of the runtime's single-slot mounting: the real apply mounts
on its own fiber, renderSlot('sidebar', ...) captures exactly the sidebar
slot's output, and update() re-renders the collapsed rail in place. The
.snap files carry semantic class names and svg fingerprints only.
The five ui-conversation machinery specs (apply-inject, chat-apply,
chat-toolview-slot, service-orchestration, selection-survival — now .tsx)
and the web app/app-shell specs assemble through the runtime instead of
hand-built Context + SlotsService + fake-session scaffolding per suite.
Session behavior mocks are typed against ISession, so incomplete fakes
fail at compile time.
A jsdom slot test runtime for feature specs: a real Cordis Context, the
production SlotsService and web-react renderer, and typed session/workspace
doubles (TestSessions implements ISessions with FixtureSession sessions;
TestWorkspaces implements IWorkspaces), so the compiler flags fixture drift
when a production face changes. Fixtures feed plain data: list rows,
conversation snapshots, and ISession-typed behavior stubs; provide-bundle
materialization runs the shared SessionProvideChannel.
DOM snapshot support: declare()/renderSlot() mount a single slot inside a
data-slot wrapper for local .snap capture, and a snapshot serializer folds
CSS-module class hashes to their semantic locals and collapses svg internals
to a content fingerprint. The typed provide() constrains declared-service
fakes to Partial of the service's outward face.
Rows keep their NATIVE button role inside a listitem seat (role overrides
exposed selectable folders as passive list items to assistive technology),
the crumb trail becomes a navigation landmark, and the path-edit input joins
the parentInert derivation so the nested create dialog cannot be undercut
through it.
The idle /plan and /plan off command paths pin the immediate-commit copy;
the chip's dead leaving/locked guard is deleted (both states disable the
button, so no click reaches the handler); the multi-turn TUI golden is
re-recorded keyless over the idle-commit wording and the standalone
plan/mode narration ordering.
- projection.spec covers applyGoalProjection's non-message early return and
the foreign-change-kind arm directly (the pure-function posture the unit
requires — a throwing apply would tear down the registry drive).
- New ui-goal browser-plugin spec exercises the plugin body over a real
cordis Context with fake faces: dock registration (id/order), verbs
reading the CAS ref from the current projected value, the
no-current-goal short circuit, RPC-error mapping, HMR disposal, the
GoalDock adapter's three projection states, and the inert node half.
Feature packages now reach these domains through interface types only:
ISession/SessionFace (identity + prompt/cancel/loadOlder + the useSession
snapshot source), ISessions, IWorkspaces, ILayout, IConversation, and the
existing SlashServiceContract now actually mounted on Context.slash. The
concrete services implement their face; wire-pump and assembly entry
points stay on the classes. The provide-channel materialization and
current-projection logic moves into SessionProvideChannel so the
production service and the client test runtime share one implementation.
The workspaces service consumes sessions through the narrow SessionsPort.
Plan mode is entered through /plan only — the select control is retired.
The conversation.input.plan seat (now right of the access-mode control)
renders a read-only Plan chip while the projection's effective target is
plan mode; its hover x executes /plan off, and the chip follows the folded
target (appears on /plan immediately, disappears on /plan off) with frames
correcting either way. While plan mode is targeted the composer textarea's
placeholder switches to the plan-task wording — InputBar reads the same
projection through the standard-kit useProjection (the TodoDock posture:
a type-only key merge, no domain service edge), and owner placeholders
still win.
Bare /permission now opens a flat popupSelect of presets (current value
active, custom excluded) instead of returning a text report — the /model
pattern on a single-level list. The new dsh-client-ui-permission package
registers the contribution; a pick submits '/permission <preset>' through
Session.command, so the picker, the composer chip, and the argued line all
write through the one host command and follow the one pushed projection
frame. Options and availability read the 'permissions' projection.
ui-command gains the hostBacked contribution mode: a same-named host
command is cooperation, not a collision — the host keeps the catalog row,
the argument claim (space and argued-enter fall through to the host path),
and the lifecycle logging, while the contribution supplies only the
bare-invocation popup. The /permission command handler keeps its bare-line
text report for host surfaces without a popup layer (TUI, raw execute).
- Dismissal (Escape/mask) is ignored while adoption is busy: the owner's
in-flight createWorkspace must not land behind an apparent cancel.
- Every parent control goes inert while the nested create dialog is open
(Modal traps no focus, so Shift-Tab/AT could close, adopt, or retarget
underneath the child).
- Creation settlements are gated on an open-generation ref: a create that
resolves or rejects after the flow closed (and possibly reopened) can no
longer relist the stale target or surface its alert in the fresh dialog.
- The keyless snapshot waits for the Open button's enabled state before
clicking — on slow runners the selection's child listing was still in
flight and the click landed on a disabled button.
- Regenerate the cordis/config catalogs, doc graphs, and module graph for
the four newly mounted plugins and the goal projection key.
- ui-goal README pair (Model Experience indirect + Known Limitations) with
its i18n record; sentence-allowlist entry for the indirect form.
- knip workspace entry for ui-goal (tsx test pattern).
- type-equiv manifest follows the seven host-coupled goal symbols to
domain.ts.
- The web-slash-command-dispatch note documents the dropped prompt
interception and leaves with it; the goal-bar note's code paths follow the
component into ui-goal (pairing re-recorded).
- Escape (and the mask) now reaches only the topmost dialog: while the
nested New-folder dialog is up the browser ignores its own Modal close,
and the nested dialog's in-flight fence keeps both open during creation.
- New folder disables while any listing loads, so a slow post-create
relist/select sequence cannot host a second create against a target the
pending listing is about to change.
- Deep ancestry scrolls inside a dedicated crumb trail whose tail is pinned
into view; the path-edit zone keeps its reserved width instead of being
clipped by the card, preserving cross-drive path entry.
- The workspace-management e2e records a directory-browser aria golden at a
staged tree (host HOME pointed at the scaffold cwd collapses ancestry into
the Home crumb, keeping the artifact machine-independent), and the keyless
snapshot's row targeting goes through visible label text — listitem
accessible-name computation differs across dom-accessibility-api
environments (the CI-only miss).
The summary blank bit switches from log emptiness to the absence of any
turn/start: standalone plugin events — command lifecycle records,
plan/mode, session titles, goal metadata — no longer surface a fresh
session in lists or steal the New Session view. Running /plan (or /goal)
on a blank session keeps it blank and reusable; the first accepted
prompt's turn clears it. Both carriers share one predicate (summarize +
the host/session-added frame); the cold path keeps its constant false
with the index-read rationale; the client mirror already flips only on
prompt acceptance and needed no change.
set() on an idle agent appends plan/mode at once — no request boundary
would arrive until the next prompt, so a queued intent used to hang as
pending forever (the composer showed a dead pending target). A running
agent keeps the boundary-flush path unchanged. set() now reports which
branch ran (committed/queued/cancelled/noop); the /plan handler's copy
follows the branch (idle: "Plan mode on/off", mid-turn: the next-step
wording), and both commit paths share the header-delta narration. The
invariant drops turn enclosure: plan/mode is a standalone whole-value
event (the synthetic log-only turns removal already established the
between-turns append shape). The fixture mirrors the idle commit.
CI's cold jsdom needs more than findByRole's 1s default between opening the
dialog and the fixture listing's first paint; the surrounding helpers already
wait 10s.
apps/cli/cordis.yml swaps bash-local/fs-local for the acp-agent composition
(sandbox-local + sandbox-policy + bash-sandbox + user-approval + permission
+ fs-sandbox; fs-policy composes on top unchanged). The deployment default
stays danger-full-access + never — byte-for-byte the old unconfined
behavior, so the replay e2e lane and demos are unaffected — while
DSH_PERMISSION_MODE opts a process into a confined default and the
/permission command switches per session. The permission preset table ships
the three product presets (read-only/ask, workspace-write/ask,
danger-full-access/never) explicitly in the deployment config.
PermissionSelect returns as the Access seat's wired occupant: options and
the current value read from the 'permissions' projection through the
standard-kit useProjection (no fetch, no mount timing — the resident
composer's mount-once fetch bug dies with the fetch), key absence renders
nothing (permission-less host, or a Draft with no session), and a pick
submits the '/permission <preset>' command line through the new
ComposerBarInjected.command callback (Session.command = command.execute
admission; the pushed projection frame lands the confirmed value). The
READONLY_OPTIONS placeholder and its local state leave InputBar.
The connection fixture mirrors the host: a permissions unit fold (three
knob events over the fixture preset table), the projections block +
baseline/push frames carry the key, and /permission joins the command
catalog with the same switch-through-knob-events handler shape.
The persisted row (sessionId, key, stateVersion, observedSeq, state)
becomes (sessionId, key, ver, seq, val) — the cache medium repeats these
three names for every unit of every session, so the long forms dominated
the JSON payload. ProjectionCheckpointRow and the checkpointRow zod spec
rename together; the domain spec bumps to v3 (cache semantics: the old
medium is discarded, not migrated). The unit-facing declaration keeps
stateVersion — only the persisted/checkpoint row shape changes.
A fresh injected face while the same request is open re-fires the effect;
the armed guard must not relaunch the chooser (the uncovered branch CI's
per-file gate flagged).
Drop the assertions eslint proved unnecessary (getByRole gains the element
type parameter instead, keeping tsc satisfied), declare the injected
setPlanMode as a function property (the seat face is this-free), and narrow
the fixture's command args without String()'s object stringification arm.