Repository navigation
Releases
Releases · ffyfox/dsh-cua-preview
v0.2.0
Fixed
- The
after the actionframe stayed below the fold after an approval. Each frame's box is now
reserved from its own durable reference —ImageAttachmentRef.width/heightare required fields, so
the size is known before the bytes are — and the loading paint is the same element with the same
geometry as the loaded one. Previously each frame painted a short text placeholder and then an unsized
<img>, so a two-frame result grew the row up to four times in a row. The conversation's shipped
follow-the-tail controller starts one smooth scroll on the first growth, skips the next growth while
that scroll is outstanding (ScrollFollow.toBottomreturns whilethis.target !== null), and then
reads the short landing as the reader having moved, releasing follow intent for good. Reserving the box
leaves exactly one growth to follow, so the app's own logic lands on the true bottom. The loading
wording is now a skeleton-filled box of the final size; a reference with no usable dimensions keeps the
text placeholder instead of claiming a size it does not know. The reservation is pinned twice: the
Client verifier asserts that the loading paint and the loaded paint are the same box, and the acceptance
harness measures it in a real browser — a reserved box is 402 px tall and decoding the bytes leaves it
at 402 px, while the old two-step paint moves the row from 42 px to 402 px. - The Client now fetches the frame route in the shipped document-relative form. The Host registers
/api/cua-preview.frame; the Client asks forapi/cua-preview.frame. Every shipped Client does this
with its own route ("/api/session/uploadFileBinary".slice(1)indsh-client-file-upload, a…_ROUTE
beside each…_PATHindsh-client-ui-deliverables). In today's shells the document sits at the origin
root, so both forms resolve to one URL and the difference is invisible — which is exactly why a live
test could not have caught it; the relative form is the one that survives a document served under a
path prefix. - A gated action made its own session unopenable, and the plugin no longer writes to the Session log at
all. The approval-time frame used to be referenced from a plugin-ownedcua/previewsession event.
That type is outside the harness'sKNOWN_SESSION_EVENT_TYPES, and the envelope'signorable: true
marker — the one mechanism that makes an out-of-repo event safely skippable — cannot be set by a live
writer:Session.append(type, data, opts)copies onlysourceEventSeqsandsurfaceOpout ofopts.
Every session that had run a gated action therefore landed on disk with an unknown, required event,
and the persistence reader refused it from then on: "contains event type "cua/preview" (seq 9403)
unknown to this harness and not marked ignorable; refusing to interpret the log". The visible effect
was a conversation whose history would not load after a harness restart, while the host kept appending
to the same log. The plugin now appends nothing; the frame reaches the Client through the call's own
tool/resultcontent, which is a known event type the Host authorizes. Also present in0.1.0. - The suite now fails if this reappears.
scripts/verify-client-bundle.mjsrejects any.append(in
src/(comments stripped, so documentation that names the API is not mistaken for a call to it) and
any dependency on the Conversation event service; the acceptance harness asserts that every event in
the live log is insideKNOWN_SESSION_EVENT_TYPESand that only the harness's own types appear; and the
real-product probe reports the same two facts from a log the shippeddshbinary wrote. The defect
survived a release because nothing had ever looked at the log the plugin produced.
Added
- The screen is on screen while the approval is being decided. The call's row now paints the current
screen from the moment the card appears, read live from the Host rather than from the log: the frame
captured an instant before the ask is held under the call id (src/pending-frames.js) and served to the
operator's own browser on an exact Fetch route,GET/HEAD/api/cua-preview.frame
(src/frame-route.js). That route is the shape the documents name for a browser-native response
(docs/api-gateway.md: "A feature that needs a streamed or browser-native response registers an exact
Connection Fetch route instead of defining a Remote method";docs/subsystems/web-client.md:
"feature-owned downloads register exact Fetch routes"), admitted only through the browser-session trust
boundary, markedno-store, and withdrawn with the plugin. The Client half reads it through a resource
of its own,dsh-resource://cua/frame/<callId>, retrying on404with a bounded backoff — the row is
mounted when the call is dispatched, a moment before the capture exists. The route stops serving a call
the instant its own result exists (the result carries the same picture), and it is skipped entirely on a
Host that mounts no such registry: the tools load and every gated action works either way.connection
is deliberately not a declared dependency, so the route attaches throughctx.injectwhen that
service appears — a one-shotctx.get('connection')insideapplywas measured to lose that race in the
real web profile, which would have silently disabled the preview in the only profile that has one. - The post-action screenshot now waits for the page to react.
page.click()returns the moment the
input is dispatched, andgotowithdomcontentloadedreturns before a JS-rendered page has rendered
anything, so the frame used to be the state the action started from: measured against a page whose
own reaction takes 900 ms, it was written 64 ms in and still readPENDING. Every gated action now
waits for a grace period, then for the network to go quiet, then for the DOM to stop changing, all
bounded by a budget (250/400/3000 ms by default). Newsettleconfig:falseskips the wait, a
number is the total budget, an object overrides any of the three values; anything else is logged and
ignored rather than failing the load. settled/settleMs/networkBusyon a grantedbrowser_navigate/browser_actresult, and a
line in the model-facing text when the budget ran out. A heuristic cannot promise a frame is final, so
the result says so instead of the plugin claiming more than it verified; withsettle: falsethe
fields are absent entirely.examples/test-page/slow.html, a fixture whose banner changes colour 900 ms after load and 900 ms
after a click. 39 new acceptance checks use it and the workspace: 24 compare the tool's frame against
two references taken from the same browser — what a no-wait capture shows, and what the settled page
shows — and require the tool's frame to be the second and not the first; one of those drives a page
that never answers, to pin the bounded-and-reportedsettled: falsepath; seven pin the newsettle
configuration surface (default, disable switch, number shorthand, partial object, and the refusal of
everything else); and the rest pin the policy-off refusal against the realApprovalService, where the
mounted answerer would have granted and is proven not to be consulted.promptsDisabledon a refusedbrowser_navigate/browser_actresult, present exactly when the
session policy meant nobody was asked, so a Client or a reader can tell the two refusals apart without
parsing the prose.
Fixed
- A browser that died no longer poisons the rest of the session. The cached browser/page pair was
believed unconditionally, so once Chrome went away (crash, OOM killer,SIGKILL, closed window) every
later call failed withAttempted to use detached Frameuntil the plugin was reloaded — including
read-only calls that raise no approval. The cache is now validated withbrowser.connectedbefore
reuse, and a live browser whose tab was closed gets another tab instead of a second browser.
page.isClosed()is deliberately not the signal: measured against aSIGKILLed Chrome it still
returnedfalsewhile every call failed. Operations that cannot have produced a side effect — and
navigation, which is harmless to repeat — are retried once against a re-established page;click,
fillandsubmitare not, because replaying an input whose delivery is unknown can double-act. - A refused
browser_navigatenamed the wrong target. The refusal printed the page the browser was
still on under arequest:label —request: navigate -> <the page you are already on>— so the model
was told that something it never asked for had been refused, in the one result that also orders it not
to retry.refusedNavigate()reporting the untouched page as a fact is right; the render cannot
borrow that fact for the request, which exists only in the call's own arguments (the dispatcher
passes them torenderas its first parameter). The refusal now statesrequest: navigate -> <asked>
followed bypage: <still open>, the same two lines the siblingbrowser_actrefusal already
produced. Also present in0.1.0: it survived because no test ever rendered this path — every
refusal check drovebrowser_act— so seven checks were added, and they were shown to fail against
the old behaviour before the fix was kept.
Changed
- A granted action now returns both frames, and the row captions each by role. The approval-time
frame is no longer a separate session event (seeFixed): it is the first image block of the
granted result, followed by the post-action frame, andpresentationMetagainedframes
(['before','after'],['before']on a refusal or when the post-action capture failed) so the Client
never has to infer a caption from an image's position or fromgrantedalone. The row therefore paints
...
v0.1.0
First release. Everything below shipped together, so the entries describe the initial state of the
project rather than a sequence of published versions — including the defects that real use found and
the checks that now pin them.
Added
browser_navigate,browser_act(click/fill/submit),browser_snapshotand
browser_screenshottools, registered into the DSH tool registry. The read-only pair never raises an
approval, because neither can change the page.- An approval gate in front of every navigating or acting call. The action is described in one line in
the UI language, and the screen is captured at ask time so the user reviews what they are approving. - Both frames are kept and returned through separate documented channels: the post-action frame as the
model-facing result image, the approval-time frame as a log-onlycua/previewsession event that the
Client reads and paints. - A Client half whose entire footprint is one hidden Conversation node and three keyed tool seats. It
registers into no shipped single-occupancy slot. - A refusal notice that names the shipped gating path's own sentence verbatim in both languages, states
that the refusal is a human decision rather than a failure, and tells the model to stop and ask the
user instead of retrying. - Three test entries: a Client-bundle verifier, an acceptance harness against the real DSH services, and
a probe that boots the shippeddshbinary with the plugin patched in. examples/test-page/— a local page with a field, a button and a form, used by the acceptance checks.- An MIT license and a
docs/design.mddesign record, plus a Chinese README (README.zh.md).
Fixed
- The approval-time frame was reported to the model as the action's result. For a navigation that
frame is always the pre-action tab, so a model could read it as a failed action. Both frames are now
kept and named. - The approved frame was described but not readable, rendering "screenshot could not be loaded":
the Host authorizes an attachment read by scanning specific event positions, andresult.metais not
one of them. The frame is now referenced from the plugin's own log-only event. - The credential could only be painted after the user had already decided, because a tool row is
settled-only. It now reaches the Client through the hidden carrier node while the approval is pending. - The two frames rendered in the wrong order. The carrier is now hidden, and the single row that
owns the call paints both frames in the order they happened. fillappended instead of replacing: triple-click does not reliably select on a plain text input.
The field is now cleared explicitly before typing.- A
singleslot collision failed a shipped Client package and broke the whole client plugin load.
The plugin now registers only into slots it owns, and the verifier asserts coexistence with the
shipped occupant. - A first navigation's credential frame was a pure-white blank tab. A blank screen now emits no
frame at all, recorded as a distinct state from a capture failure. - A refusal never reached the model. A
required: trueannotation on a shared schema node made two
optional image fields mandatory, so a refusal failed output validation and the model received a
schema error instead of the refusal — which is what invited a retry against a user who had just said
no. The annotation is removed, the fields are genuinely optional, and the refusal path is now
dispatched through the tool pipeline in every harness. - The Client row crashed on a
nullblock during replay; it now renders without throwing.
Changed
- The tool descriptions state the screenshot timing, and the gated tools state the stop-and-ask rule, so
both are in the system prompt before the model acts rather than only in a result afterwards. - The approval
reasoncarries only the action description: no file path, dimensions or byte count. The
shipped panel renders that string as the card's headline, so it stays one line. - Target naming follows the page's own accessible name instead of the CSS selector alone, and
input.valueis excluded from that chain except for submit-style inputs. - Tests depend on the published DSH packages as dev dependencies, so the suite is reproducible on any
machine instead of relying on one local install.
Notes
- The plugin is a DSH bundle: it ships a configuration layer, and a profile is what boots it.
- This is an unofficial community work, not affiliated with, authorized, sponsored or endorsed by
Z.AI Co., Ltd or DeepSeek. The approval architecture follows ZCode'scua-permission-brokeras a
design borrowing; no ZCode source is copied.