The one lifecycle for every operator-facing View.
A View has a CLOSED semantic document, a stable id, and the same open, patch
and close rail on every channel. Its :kind declares the capability policy:
:input is Human Input — a typed form that BLOCKS until a submit or
cancel action, timeout, or interruption;:live is a non-blocking picture driven by its producer and optionally ended
by an interrupt action.The distinction belongs in policy, not transport. Both kinds share the pending
registry, publish :view/open / :view/patch / :view/close envelopes, and
receive operator intent through action!. Renderers dispatch on :kind and
never infer behavior from an event name or resource path.
This namespace PARSES extension data against the CLOSED vocabulary declared by [[com.blockether.vis.contract.view]]. Input answers are coerced and checked once at the settle seam; live patches are normalized and materialized once before any surface sees them. Unknown keys are refused, while every declared key is preserved through normalization.
Secrets never travel as plaintext. A :password and an :otp field resolve to
an opaque vis-secret:<uuid> handle; plaintext stays in a process-local vault
and is readable only through reveal-secret from the trusted extension side.
The one lifecycle for every operator-facing View. A View has a CLOSED semantic document, a stable id, and the same `open`, `patch` and `close` rail on every channel. Its `:kind` declares the capability policy: - `:input` is Human Input — a typed form that BLOCKS until a `submit` or `cancel` action, timeout, or interruption; - `:live` is a non-blocking picture driven by its producer and optionally ended by an `interrupt` action. The distinction belongs in policy, not transport. Both kinds share the pending registry, publish `:view/open` / `:view/patch` / `:view/close` envelopes, and receive operator intent through [[action!]]. Renderers dispatch on `:kind` and never infer behavior from an event name or resource path. This namespace PARSES extension data against the CLOSED vocabulary declared by [[com.blockether.vis.contract.view]]. Input answers are coerced and checked once at the settle seam; live patches are normalized and materialized once before any surface sees them. Unknown keys are refused, while every declared key is preserved through normalization. Secrets never travel as plaintext. A `:password` and an `:otp` field resolve to an opaque `vis-secret:<uuid>` handle; plaintext stays in a process-local vault and is readable only through [[reveal-secret]] from the trusted extension side.
(action! view-id raw)Apply one operator action to open View view-id through the shared action seam.
submit and cancel are input policy; select, activate and interrupt are live policy.
The action map is closed and accepts snake_case string keys or kebab keywords.
Every outcome carries :action, :view-id, and :is-accepted; malformed or
kind-incompatible actions are refused before they can mutate the View.
Apply one operator action to open View `view-id` through the shared action seam. `submit` and `cancel` are input policy; `select`, `activate` and `interrupt` are live policy. The action map is closed and accepts snake_case string keys or kebab keywords. Every outcome carries `:action`, `:view-id`, and `:is-accepted`; malformed or kind-incompatible actions are refused before they can mutate the View.
(adopt-open-views! home)Give every live view still open and not yet homed the iteration home — where
a close arriving after its block is gone files its artifact. Returns the ids
it stamped, and is called when a block ENDS, the moment that id exists.
A view OUTLIVES the block that opened it: the call returns while the run keeps painting, and the human's stop can arrive an hour later on a gateway thread. That block's artifact collector was drained the instant it returned, so a close reaching only the collector files a row nobody will ever read — the picture the human watched, lost at the moment they stopped it.
Give every live view still open and not yet homed the iteration `home` — where a close arriving after its block is gone files its artifact. Returns the ids it stamped, and is called when a block ENDS, the moment that id exists. A view OUTLIVES the block that opened it: the call returns while the run keeps painting, and the human's stop can arrive an hour later on a gateway thread. That block's artifact collector was drained the instant it returned, so a close reaching only the collector files a row nobody will ever read — the picture the human watched, lost at the moment they stopped it.
(answer->wire answer)Wire projection of a request! answer: snake_case string keys, JSON-safe
values. :password and :otp values stay opaque handles.
Wire projection of a [[request!]] answer: snake_case string keys, JSON-safe values. `:password` and `:otp` values stay opaque handles.
(cancel! request-id)(cancel! request-id reason)Cancel pending request request-id on the operator's behalf. Returns true
when it was pending AND dismissable.
A request declared :is-cancellable false refuses here, so EVERY surface is
refused alike — the TUI dialog, the companion app, any extension API. The
only ways out of such a request are an accepted answer, its timeout, and
cancel-all!.
Cancel pending request `request-id` on the operator's behalf. Returns true when it was pending AND dismissable. A request declared `:is-cancellable false` refuses here, so EVERY surface is refused alike — the TUI dialog, the companion app, any extension API. The only ways out of such a request are an accepted answer, its timeout, and [[cancel-all!]].
(cancel-all!)(cancel-all! reason)Cancel every pending request. Returns how many were released. Used when a
channel detaches or the session shuts down, so no thread stays parked — this
one ignores :is-cancellable, because nothing is left to answer with.
Cancel every pending request. Returns how many were released. Used when a channel detaches or the session shuts down, so no thread stays parked — this one ignores `:is-cancellable`, because nothing is left to answer with.
(close-abandoned! known ending)Close every live view open right now whose id is NOT in known, with
ending. Returns the VERDICT of each one, oldest first — the finished picture
included, because the run that would have read it is the one that died.
The run that opens a view is the one that ends it — with vis.live closes on
the way out. A block killed at its eval wall or cancelled mid-flight reaches
no such line: its frames unwind through a context that no longer accepts host
calls, so the close never arrives and the view stays open with nothing left to
patch it — a pane painting a picture that will never move again, over a Stop
nobody is listening to. That end, and only that end, sweeps: a view
deliberately carried from one block to the next is known and stays open.
Close every live view open right now whose id is NOT in `known`, with `ending`. Returns the VERDICT of each one, oldest first — the finished picture included, because the run that would have read it is the one that died. The run that opens a view is the one that ends it — `with vis.live` closes on the way out. A block killed at its eval wall or cancelled mid-flight reaches no such line: its frames unwind through a context that no longer accepts host calls, so the close never arrives and the view stays open with nothing left to patch it — a pane painting a picture that will never move again, over a Stop nobody is listening to. That end, and only that end, sweeps: a view deliberately carried from one block to the next is `known` and stays open.
(close-live! view-id)(close-live! view-id ending)(close-live! view-id ending human)End live view view-id and return what the model should read. nil when the
view was already closed, so a finally closing what an interrupt already
closed is a no-op rather than a second result.
ending says how it ended: :reason (completed by default), :summary,
:error, :artifact-id, optional archive-only :selection-snapshots, and optional
compact string :model-result. A snapshot's PICTURE is read back from the wire
spelling first — the only view an extension holds is the one state answered it,
in the JSON it crossed with. The full verdict always remains in the artifact
and human-facing close event. human is the person who stopped it — {:note …},
which only interrupt-live! passes, because a run does not get to claim a
human ended it.
The close SETTLES the view: the record it has been writing since open becomes
an artifact this session owns, and its id rides back in the verdict — so the
human can reopen the log after the pane is gone instead of it being dumped into
the transcript. A close reached from somewhere that holds no artifacts (a human's
stop arriving on a gateway thread) still files one, into the collector the OPENING
block captured. Only a view opened outside any block names no artifact; the record
is sealed either way, and every surface still reaches the log by view id.
End live view `view-id` and return what the model should read. nil when the
view was already closed, so a `finally` closing what an interrupt already
closed is a no-op rather than a second result.
`ending` says how it ended: `:reason` (`completed` by default), `:summary`,
`:error`, `:artifact-id`, optional archive-only `:selection-snapshots`, and optional
compact string `:model-result`. A snapshot's PICTURE is read back from the wire
spelling first — the only view an extension holds is the one `state` answered it,
in the JSON it crossed with. The full verdict always remains in the artifact
and human-facing close event. `human` is the person who stopped it — `{:note …}`,
which only [[interrupt-live!]] passes, because a run does not get to claim a
human ended it.
The close SETTLES the view: the record it has been writing since `open` becomes
an artifact this session owns, and its id rides back in the verdict — so the
human can reopen the log after the pane is gone instead of it being dumped into
the transcript. A close reached from somewhere that holds no artifacts (a human's
stop arriving on a gateway thread) still files one, into the collector the OPENING
block captured. Only a view opened outside any block names no artifact; the record
is sealed either way, and every surface still reaches the log by view id.(coerce-value {:keys [type] :as field} value)Coerce and validate one raw value against normalized field. Returns
[:ok coerced] or [:error message].
Coerce and validate one raw `value` against normalized `field`. Returns `[:ok coerced]` or `[:error message]`.
(coerce-values fields values)validate-values for a SUBMISSION: identical answer, except that accepted
:password and :otp values are replaced with opaque vault handles.
[[validate-values]] for a SUBMISSION: identical answer, except that accepted `:password` and `:otp` values are replaced with opaque vault handles.
How long a request waits when its spec says nothing: five minutes — long
enough for a human who is reading, short enough that a dialog nobody noticed
does not park an extension all afternoon. A caller who wants another budget
names its own :timeout-ms, or no-timeout-ms to wait as long as it takes.
How long a request waits when its spec says nothing: five minutes — long enough for a human who is reading, short enough that a dialog nobody noticed does not park an extension all afternoon. A caller who wants another budget names its own `:timeout-ms`, or [[no-timeout-ms]] to wait as long as it takes.
(forget-secret! handle)Drop the plaintext behind handle. Returns true when something was dropped.
Drop the plaintext behind `handle`. Returns true when something was dropped.
(forget-secrets!)Empty the vault. Returns how many plaintexts were dropped.
Empty the vault. Returns how many plaintexts were dropped.
(indefinite-timeout? timeout-ms)True when timeout-ms is no-timeout-ms: the request waits indefinitely.
True when `timeout-ms` is [[no-timeout-ms]]: the request waits indefinitely.
(input-fields fields)Every ANSWERABLE field in fields, depth-first in the order a surface draws
them. A group carries no value, so it is walked through and never returned:
this is the sequence that keys :values, and the reason a layout change can
never change an extension's answer map.
The hot path of the whole module — every keystroke on every surface re-validates through here — so it is a transient walk, not a lazy one.
Every ANSWERABLE field in `fields`, depth-first in the order a surface draws them. A group carries no value, so it is walked through and never returned: this is the sequence that keys `:values`, and the reason a layout change can never change an extension's answer map. The hot path of the whole module — every keystroke on every surface re-validates through here — so it is a transient walk, not a lazy one.
(interrupt-live! view-id)(interrupt-live! view-id note)End an extension-owned live view because a human stopped watching.
End an extension-owned live view because a human stopped watching.
(live-dispatch opts)One live-view op: an options map with wire keys in, the answer map out.
open answers the mounted view, patch the sequence number the engine
accepted, state what the view looks like right now, and close the verdict
the model reads. Every answer says :is-open, so the caller learns from the
op it was making that the human stopped watching — the record's verdict comes
back with it.
One live-view op: an options map with wire keys in, the answer map out. `open` answers the mounted view, `patch` the sequence number the engine accepted, `state` what the view looks like right now, and `close` the verdict the model reads. Every answer says `:is-open`, so the caller learns from the op it was making that the human stopped watching — the record's verdict comes back with it.
(live-json! envelope-json)The strings-only seam a Python extension crosses for a live view: one JSON envelope in, one JSON answer out.
Channel routing is host-side — a channel_id/channel_ids key is dropped
rather than minting keywords from guest data — so an extension always reaches
the channels the host picked, exactly as request-json! does.
Only a state with timeout_ms blocks: it waits for a change or close,
without periodic guest callbacks or publishing a view event.
The strings-only seam a Python extension crosses for a live view: one JSON envelope in, one JSON answer out. Channel routing is host-side — a `channel_id`/`channel_ids` key is dropped rather than minting keywords from guest data — so an extension always reaches the channels the host picked, exactly as [[request-json!]] does. Only a `state` with `timeout_ms` blocks: it waits for a change or close, without periodic guest callbacks or publishing a view event.
(live-nodes? nodes)Whether nodes are the picture a human WATCHES rather than the questions a
form asks. A layout group is the ONE node both vocabularies share, so a
builder composing one asks its children which check it is dated against — and
a group inside a group answers by its own children.
Whether `nodes` are the picture a human WATCHES rather than the questions a form asks. A layout group is the ONE node both vocabularies share, so a builder composing one asks its children which check it is dated against — and a group inside a group answers by its own children.
(live-patch<-wire wire)Inverse of the wire projection for one accepted patch — what a
view.patch session event carries. The gateway COALESCES patches
before it journals them, so :seq names the last engine patch the frame folded
in, and a surface that already applied it can tell.
Inverse of the wire projection for one accepted patch — what a `view.patch` session event carries. The gateway COALESCES patches before it journals them, so `:seq` names the last engine patch the frame folded in, and a surface that already applied it can tell.
(live-result<-wire wire)Inverse of the wire projection for a view's VERDICT — what a view.close
session event carries.
Inverse of the wire projection for a view's VERDICT — what a `view.close` session event carries.
(live-view view-id)What live view view-id looks like right now, or nil. This IS what the
surfaces paint: one materialized map, so the terminal, the phone and the model
cannot disagree about a row.
What live view `view-id` looks like right now, or nil. This IS what the surfaces paint: one materialized map, so the terminal, the phone and the model cannot disagree about a row.
(live-view<-wire wire)Inverse of the wire projection for a MATERIALIZED live view — the canonical
snake_case map a view.open session event carries, and the shape
the live-views resync answers with.
A run SHOWING its work inside vis-agent serve publishes on an in-process channel
bus that never leaves that JVM, so for every other process this event IS the
view. view<-wire is the same door for a form.
Inverse of the wire projection for a MATERIALIZED live view — the canonical snake_case map a `view.open` session event carries, and the shape the live-views resync answers with. A run SHOWING its work inside `vis-agent serve` publishes on an in-process channel bus that never leaves that JVM, so for every other process this event IS the view. [[view<-wire]] is the same door for a form.
(live-views)Every live view open right now, materialized exactly as the surfaces paint them, oldest first.
This is the resync: a client that joined mid-flight, woke from sleep or lost its stream reads the picture back instead of replaying every patch it missed.
Every live view open right now, materialized exactly as the surfaces paint them, oldest first. This is the resync: a client that joined mid-flight, woke from sleep or lost its stream reads the picture back instead of replaying every patch it missed.
The :timeout-ms that means NO deadline at all: request! parks until a
human answers, a surface cancels, or the surrounding turn is interrupted.
Nothing infers it. A spec asks for it explicitly with timeout_ms 0, so a
spec that merely FORGOT the key still expires at default-timeout-ms
instead of silently pinning the run on an operator who walked away.
The `:timeout-ms` that means NO deadline at all: [[request!]] parks until a human answers, a surface cancels, or the surrounding turn is interrupted. Nothing infers it. A spec asks for it explicitly with `timeout_ms` 0, so a spec that merely FORGOT the key still expires at [[default-timeout-ms]] instead of silently pinning the run on an operator who walked away.
(normalize-field field)Validate one FIELD spec — a leaf holding exactly one answer — and return its
internal form. Throws ex-info with :type :vis/view-invalid-field
on a bad spec.
Three names, three jobs, and every field ends up with all three:
:name is how the answer is KEYED — the key the extension reads back out
of :values (:id is the historical alias, accepted and still emitted).:label is how the field is SHOWN. Never blank: a field without one
shows its :name, so no surface ever draws a bare, unlabelled input.:description is the prose under that label, rendered in italic by every
dialog.A layout group is not a field and never arrives here: normalize-node
routes it to [[normalize-group]] before a single value key is parsed.
Validate one FIELD spec — a leaf holding exactly one answer — and return its
internal form. Throws `ex-info` with `:type :vis/view-invalid-field`
on a bad spec.
Three names, three jobs, and every field ends up with all three:
- `:name` is how the answer is KEYED — the key the extension reads back out
of `:values` (`:id` is the historical alias, accepted and still emitted).
- `:label` is how the field is SHOWN. Never blank: a field without one
shows its `:name`, so no surface ever draws a bare, unlabelled input.
- `:description` is the prose under that label, rendered in italic by every
dialog.
A layout group is not a field and never arrives here: [[normalize-node]]
routes it to [[normalize-group]] before a single value key is parsed.(normalize-live-node node)One declared node, refused where it was BUILT — the seam the node builders in
com.blockether.vis.view validate through, so an unknown key or a
:tone outside the table throws at the line that wrote it rather than in
front of the human.
One declared node, refused where it was BUILT — the seam the node builders in `com.blockether.vis.view` validate through, so an unknown key or a `:tone` outside the table throws at the line that wrote it rather than in front of the human.
(normalize-live-op op)One patch operation, refused where it was BUILT — the seam the add-node /
remove-node builders check through. Shape only: whether the node an op
names EXISTS is the running view's answer, in normalize-patch.
One patch operation, refused where it was BUILT — the seam the `add-node` / `remove-node` builders check through. Shape only: whether the node an op names EXISTS is the running view's answer, in [[normalize-patch]].
(normalize-live-view view)Validate a live-view spec and return the view the materializer holds. Throws
ex-info with :type :vis/view-invalid-live-view on a bad spec, so a
view is refused WHERE IT WAS DECLARED, before any surface drew anything.
Three keys are the ENGINE's stamps and a spec that writes one is refused: the
view's :id, the :seq no patch has advanced yet, and :created-at. Two
writers of one id is exactly how a patch lands on the wrong view.
Unlike a request, a view must name its SESSION here rather than at mount time: the session is where the record is kept, so a view without one has nowhere to be written down.
:timeout-ms defaults to no-timeout-ms — a build takes as long as the
build takes. It is the deadline a surface may show and a driver may honour;
nothing in the engine kills a view that is still being patched, because the
work behind it is the extension's, not a human's.
Validate a live-view spec and return the view the materializer holds. Throws `ex-info` with `:type :vis/view-invalid-live-view` on a bad spec, so a view is refused WHERE IT WAS DECLARED, before any surface drew anything. Three keys are the ENGINE's stamps and a spec that writes one is refused: the view's `:id`, the `:seq` no patch has advanced yet, and `:created-at`. Two writers of one id is exactly how a patch lands on the wrong view. Unlike a request, a view must name its SESSION here rather than at mount time: the session is where the record is kept, so a view without one has nowhere to be written down. `:timeout-ms` defaults to [[no-timeout-ms]] — a build takes as long as the build takes. It is the deadline a surface may show and a driver may honour; nothing in the engine kills a view that is still being patched, because the work behind it is the extension's, not a human's.
(normalize-node node)Validate one node of a request's field TREE and return its internal form.
This is the fork the whole shape hangs on, and it is taken ONCE, up here: a
group is control flow — it arranges the children below it and holds no
answer — a heading or a paragraph is pure decoration that neither asks
nor arranges, and anything else is a field holding exactly one answer.
Deciding it above the three normalizers is why normalize-field never has
to ask whether it is really layout or ink, and why no value path below
carries a branch for a node that can never take one.
Validate one node of a request's field TREE and return its internal form. This is the fork the whole shape hangs on, and it is taken ONCE, up here: a `group` is control flow — it arranges the children below it and holds no answer — a `heading` or a `paragraph` is pure decoration that neither asks nor arranges, and anything else is a field holding exactly one answer. Deciding it above the three normalizers is why [[normalize-field]] never has to ask whether it is really layout or ink, and why no value path below carries a branch for a node that can never take one.
(normalize-patch view patch)Validate patch against the view it lands on and return its internal form:
the operations as declared, under the engine's own :view-id and the NEXT
:seq.
The ops may arrive bare or under :ops, because the wire carries a map and a
Clojure caller has a vector — everything below that is one vocabulary. Shape
only: whether the node an op names EXISTS, and whether it would cross a
bound, is materializer/apply-patch's answer.
Validate `patch` against the `view` it lands on and return its internal form: the operations as declared, under the engine's own `:view-id` and the NEXT `:seq`. The ops may arrive bare or under `:ops`, because the wire carries a map and a Clojure caller has a vector — everything below that is one vocabulary. Shape only: whether the node an op names EXISTS, and whether it would cross a bound, is `materializer/apply-patch`'s answer.
(normalize-request request)Validate an input View request spec and return its internal form. Throws
ex-info with View-owned error data on a bad spec.
Validate an input View request spec and return its internal form. Throws `ex-info` with View-owned error data on a bad spec.
(open-live! view)Mount a live view the human WATCHES and return it materialized, :id and all.
The caller keeps that id: every patch, and the close, name it.
Nothing blocks. A view nobody has mounted a surface for still runs and still
ends in the verdict the model reads — the engine says so ONCE, in the log,
rather than refusing work whose whole product is the picture at the end. That
is the one place a view differs from a request, which answers undeliverable
at once because a form nobody can see is a thread parked forever.
Mount a live view the human WATCHES and return it materialized, `:id` and all. The caller keeps that id: every patch, and the close, name it. Nothing blocks. A view nobody has mounted a surface for still runs and still ends in the verdict the model reads — the engine says so ONCE, in the log, rather than refusing work whose whole product is the picture at the end. That is the one place a view differs from a request, which answers `undeliverable` at once because a form nobody can see is a thread parked forever.
(open-live-ids)The id of every live view open right now.
The cheap half of live-views: a caller that only has to notice a view
APPEARING — the run that opened it, on its way out — never materializes one.
The id of every live view open right now. The cheap half of [[live-views]]: a caller that only has to notice a view APPEARING — the run that opened it, on its way out — never materializes one.
(open-view view-id)The open View view-id as {:kind … :view …}, or nil. The descriptor is for
capability policy and routing; renderers still receive the document itself.
The open View `view-id` as `{:kind … :view …}`, or nil. The descriptor is for
capability policy and routing; renderers still receive the document itself.(patch-live! view-id patch)Apply patch to live view view-id and return the view it made.
One patch at a time per view: the record on disk is the order the engine ACCEPTED patches in, and two threads racing would leave a file no replay can trust. The line is written BEFORE any surface is told, so a crash keeps what the engine accepted rather than what a screen managed to paint.
Apply `patch` to live view `view-id` and return the view it made. One patch at a time per view: the record on disk is the order the engine ACCEPTED patches in, and two threads racing would leave a file no replay can trust. The line is written BEFORE any surface is told, so a crash keeps what the engine accepted rather than what a screen managed to paint.
(pending-request request-id)The pending request request-id, as a view, or nil.
The pending request `request-id`, as a view, or nil.
(pending-requests)The forms a run is BLOCKED on right now, oldest first.
Live views share this registry — one id space, one cancel path — but they are
not questions: nothing is parked on them and nobody owes them an answer, so
they come back from live-views and a surface listing what WAITS on the
human never shows one.
The forms a run is BLOCKED on right now, oldest first. Live views share this registry — one id space, one cancel path — but they are not questions: nothing is parked on them and nobody owes them an answer, so they come back from [[live-views]] and a surface listing what WAITS on the human never shows one.
(request! request)Ask the operator for typed values and BLOCK until they answer.
request is a spec map — :title, :fields, optional :description,
:submit-label, :cancel-label, :is-cancellable, :timeout-ms,
:channel-ids (string keys from the Python boundary work too).
Every field carries :name, :type, :label and an optional
:description. :name keys the answer in :values, :label is what the
dialog shows above the input, and :description is the italic line under
that label — see normalize-field.
Publishes a :view/open channel event, waits for submit! /
cancel!, and always returns a map, either
:is-submitted true, :reason "submitted", plus :request-id and :values
or
:is-submitted false, :reason "cancelled"/"timeout"/"undeliverable"/…, plus :request-id
"undeliverable" is the honest answer when the event reached ZERO
listeners: no surface is mounted on any channel the request names, so no
dialog can be drawn and nobody can answer. That returns AT ONCE and logs an
error naming the request — parking the caller for the full timeout would
report a run nobody was ever shown as if a human had ignored it.
:timeout-ms is the wait this call is willing to bill: default-timeout-ms
when the spec says nothing, or no-timeout-ms (0) to wait INDEFINITELY. A
finite wait that runs out settles the request itself — the dialog closes on
every surface and the answer reads timeout, so the extension resumes with one
clear fixed outcome instead of a half-open form nobody can answer. An
indefinite wait never gives up on the human: only an answer, a cancel or an
interrupt releases it.
:password and :otp values in :values are opaque handles — see
reveal-secret.
A request MUST name the session it parks — :session-id, or the session of
the extension environment currently executing. A run nobody can attribute is
a run the companion app is never told about: the gateway bridge turns the
request into a session event, and a session event with no session has nowhere
to go, so only a surface mounted in this very process could ever answer it.
That is refused here, before anything blocks.
Ask the operator for typed values and BLOCK until they answer. `request` is a spec map — `:title`, `:fields`, optional `:description`, `:submit-label`, `:cancel-label`, `:is-cancellable`, `:timeout-ms`, `:channel-ids` (string keys from the Python boundary work too). Every field carries `:name`, `:type`, `:label` and an optional `:description`. `:name` keys the answer in `:values`, `:label` is what the dialog shows above the input, and `:description` is the italic line under that label — see [[normalize-field]]. Publishes a `:view/open` channel event, waits for [[submit!]] / [[cancel!]], and always returns a map, either :is-submitted true, :reason "submitted", plus :request-id and :values or :is-submitted false, :reason "cancelled"/"timeout"/"undeliverable"/…, plus :request-id `"undeliverable"` is the honest answer when the event reached ZERO listeners: no surface is mounted on any channel the request names, so no dialog can be drawn and nobody can answer. That returns AT ONCE and logs an error naming the request — parking the caller for the full timeout would report a run nobody was ever shown as if a human had ignored it. `:timeout-ms` is the wait this call is willing to bill: [[default-timeout-ms]] when the spec says nothing, or [[no-timeout-ms]] (0) to wait INDEFINITELY. A finite wait that runs out settles the request itself — the dialog closes on every surface and the answer reads `timeout`, so the extension resumes with one clear fixed outcome instead of a half-open form nobody can answer. An indefinite wait never gives up on the human: only an answer, a cancel or an interrupt releases it. `:password` and `:otp` values in `:values` are opaque handles — see [[reveal-secret]]. A request MUST name the session it parks — `:session-id`, or the session of the extension environment currently executing. A run nobody can attribute is a run the companion app is never told about: the gateway bridge turns the request into a session event, and a session event with no session has nowhere to go, so only a surface mounted in this very process could ever answer it. That is refused here, before anything blocks.
(request->view request)The channel/wire-facing projection of a pending request: the spec a dialog needs, and nothing a channel must not see (no promise, no submitted values, and no validator — validation is CODE the engine runs when the form is confirmed, a function cannot cross the wire, and a surface's job is to render the errors it is handed rather than to invent its own).
The field TREE is projected as a tree: a group crosses the wire with its own
:direction and :fields, so both surfaces lay the form out from the same
data instead of each inventing a layout.
The channel/wire-facing projection of a pending request: the spec a dialog needs, and nothing a channel must not see (no promise, no submitted values, and no validator — validation is CODE the engine runs when the form is confirmed, a function cannot cross the wire, and a surface's job is to render the errors it is handed rather than to invent its own). The field TREE is projected as a tree: a group crosses the wire with its own `:direction` and `:fields`, so both surfaces lay the form out from the same data instead of each inventing a layout.
(request-json! request-json)(request-json! request-json validators-json run)The strings-only seam a Python extension crosses: a JSON request object in, a
JSON answer object out. Blocks exactly like request!.
Channel routing is host-side — a channel_id/channel_ids key is dropped
rather than minting keywords from guest data, so a Python extension always
reaches the channels the host picked.
Validation is CODE, so it does not travel as JSON either: validators-json is
{field name -> how many validators that field declared} and run is called
(run field-name index value values) to reach the extension's own function,
answering the verdict
com.blockether.vis.internal.view.validation/check understands
(nil/true, a message string, false, or a throw). Only a name, an index and the
value being judged ever cross.
The strings-only seam a Python extension crosses: a JSON request object in, a
JSON answer object out. Blocks exactly like [[request!]].
Channel routing is host-side — a `channel_id`/`channel_ids` key is dropped
rather than minting keywords from guest data, so a Python extension always
reaches the channels the host picked.
Validation is CODE, so it does not travel as JSON either: `validators-json` is
`{field name -> how many validators that field declared}` and `run` is called
`(run field-name index value values)` to reach the extension's own function,
answering the verdict
[[com.blockether.vis.internal.view.validation/check]] understands
(nil/true, a message string, false, or a throw). Only a name, an index and the
value being judged ever cross.(reveal-secret handle)Return the plaintext behind a vis-secret: handle, or nil when the handle is
unknown/forgotten. Trusted-side only: never hand the result to a channel, a
log, or the model.
Return the plaintext behind a `vis-secret:` handle, or nil when the handle is unknown/forgotten. Trusted-side only: never hand the result to a channel, a log, or the model.
(select-live! view-id node-id item-ids)Select item-ids in selectable table node-id of open view view-id.
This is an ordinary durable set patch, not app-local selection: the writer
sees the change through state, and every TUI or Companion painting the view
receives the same new selection. Stale row ids and non-selectable tables are
refused by the materializer without changing the view.
Select `item-ids` in selectable table `node-id` of open view `view-id`. This is an ordinary durable `set` patch, not app-local selection: the writer sees the change through `state`, and every TUI or Companion painting the view receives the same new selection. Stale row ids and non-selectable tables are refused by the materializer without changing the view.
(set-late-artifact-filer! f)Install what a LATE close files its artifact through: called
[iteration-id attachment], answering the stored row or nil.
Until one is installed a late close still seals its record and still answers its verdict — only the row that LISTS the artifact is missing.
Install what a LATE close files its artifact through: called `[iteration-id attachment]`, answering the stored row or nil. Until one is installed a late close still seals its record and still answers its verdict — only the row that LISTS the artifact is missing.
(submit! request-id values)Resolve pending request request-id with a raw field id -> value map.
Returns {:is-accepted false :errors {field-id message}} when a value fails
its field's validation — the request stays pending so the dialog can show the
errors inline. Returns {:is-accepted true} once the waiter is released, and
{:is-accepted false :reason "unknown"} for an already-settled request.
Resolve pending request `request-id` with a raw `field id -> value` map.
Returns `{:is-accepted false :errors {field-id message}}` when a value fails
its field's validation — the request stays pending so the dialog can show the
errors inline. Returns `{:is-accepted true}` once the waiter is released, and
`{:is-accepted false :reason "unknown"}` for an already-settled request.(validate-values fields raw-values)Coerce and validate a raw field id -> value map against a request's
fields. Returns {:is-accepted true :values …} or
{:is-accepted false :errors {id message}}.
fields may be the request's TREE: layout groups hold no answer, so they are
flattened away here and a group can never change what an extension reads.
Pure — no vault, no state, no side effect of any kind — but not free: it runs
the extension's own validator FUNCTIONS. So it runs ONCE, when the human
confirms the form, never on a keystroke; only a real submission goes through
coerce-values.
Coerce and validate a raw `field id -> value` map against a request's
`fields`. Returns `{:is-accepted true :values …}` or
`{:is-accepted false :errors {id message}}`.
`fields` may be the request's TREE: layout groups hold no answer, so they are
flattened away here and a group can never change what an extension reads.
Pure — no vault, no state, no side effect of any kind — but not free: it runs
the extension's own validator FUNCTIONS. So it runs ONCE, when the human
confirms the form, never on a keystroke; only a real submission goes through
[[coerce-values]].(view<-wire wire)Inverse of request->view for a view that CROSSED A PROCESS BOUNDARY — the
canonical snake_case map a view.open session event carries.
A run parked inside vis-agent serve publishes on an in-process channel bus that
never leaves that JVM, so for every other process the session event IS the
request. Rebuilding the view goes back through the engine's own parser rather
than through a second field vocabulary; only the stamps of request!,
which normalize-request refuses by contract, are lifted across unchanged.
Inverse of [[request->view]] for a view that CROSSED A PROCESS BOUNDARY — the canonical snake_case map a `view.open` session event carries. A run parked inside `vis-agent serve` publishes on an in-process channel bus that never leaves that JVM, so for every other process the session event IS the request. Rebuilding the view goes back through the engine's own parser rather than through a second field vocabulary; only the stamps of [[request!]], which `normalize-request` refuses by contract, are lifted across unchanged.
(with-live! view body)Open the view view declares, hand its id to body, and CLOSE it — on a
throw as well as on a return, because an extension that dies mid-run must
still leave the model the picture the human watched. Returns the verdict.
A body that closed the view itself WINS: the ending it chose is the one
that ships, this close is then a no-op and the answer is nil — a run that
knows why it stopped never has that reason overwritten by its wrapper.
Open the view `view` declares, hand its id to `body`, and CLOSE it — on a throw as well as on a return, because an extension that dies mid-run must still leave the model the picture the human watched. Returns the verdict. A `body` that closed the view itself WINS: the ending it chose is the one that ships, this close is then a no-op and the answer is nil — a run that knows why it stopped never has that reason overwritten by its wrapper.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |