Skip to content

Releases

v0.2.0

Choose a tag to compare

@ffyfox ffyfox released this 03 Oct 02:17

Fixed

  • The after the action frame stayed below the fold after an approval. Each frame's box is now
    reserved from its own durable reference — ImageAttachmentRef.width/height are 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.toBottom returns while this.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 for api/cua-preview.frame. Every shipped Client does this
    with its own route ("/api/session/uploadFileBinary".slice(1) in dsh-client-file-upload, a …_ROUTE
    beside each …_PATH in dsh-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-owned cua/preview session event.
    That type is outside the harness's KNOWN_SESSION_EVENT_TYPES, and the envelope's ignorable: 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 only sourceEventSeqs and surfaceOp out of opts.
    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/result content, which is a known event type the Host authorizes. Also present in 0.1.0.
  • The suite now fails if this reappears. scripts/verify-client-bundle.mjs rejects 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 inside KNOWN_SESSION_EVENT_TYPES and that only the harness's own types appear; and the
    real-product probe reports the same two facts from a log the shipped dsh binary 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, marked no-store, and withdrawn with the plugin. The Client half reads it through a resource
    of its own, dsh-resource://cua/frame/<callId>, retrying on 404 with 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 through ctx.inject when that
    service appears — a one-shot ctx.get('connection') inside apply was 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, and goto with domcontentloaded returns 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 read PENDING. 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). New settle config: false skips 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 / networkBusy on a granted browser_navigate / browser_act result, 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; with settle: false the
    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-reported settled: false path; seven pin the new settle
    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 real ApprovalService, where the
    mounted answerer would have granted and is proven not to be consulted.
  • promptsDisabled on a refused browser_navigate / browser_act result, 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 with Attempted to use detached Frame until the plugin was reloaded — including
    read-only calls that raise no approval. The cache is now validated with browser.connected before
    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 a SIGKILLed Chrome it still
    returned false while 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,
    fill and submit are not, because replaying an input whose delivery is unknown can double-act.
  • A refused browser_navigate named the wrong target. The refusal printed the page the browser was
    still on under a request: 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 to render as its first parameter). The refusal now states request: navigate -> <asked>
    followed by page: <still open>, the same two lines the sibling browser_act refusal already
    produced. Also present in 0.1.0: it survived because no test ever rendered this path — every
    refusal check drove browser_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 (see Fixed): it is the first image block of the
    granted result, followed by the post-action frame, and presentationMeta gained frames
    (['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 from granted alone. The row therefore paints
    ...
Read more

v0.1.0

Choose a tag to compare

@ffyfox ffyfox released this 03 Oct 02:17

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_snapshot and
    browser_screenshot tools, 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-only cua/preview session 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 shipped dsh binary 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.md design 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, and result.meta is 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.
  • fill appended instead of replacing: triple-click does not reliably select on a plain text input.
    The field is now cleared explicitly before typing.
  • A single slot 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: true annotation 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 null block 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 reason carries 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.value is 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's cua-permission-broker as a
    design borrowing; no ZCode source is copied.