Files
Hare 84cb5904df feat(tui): reconcile scoped selection and publication lifetimes
Reuse canonical escaped absolute reference labels; retain structured operation selection across loading and row movement. Freeze editor targets and retire confirmation on Client observation invalidation. Mirror retained stale/loading Object, tree and Interface state without ref fabrication or automatic execution.

Validation: 36 TUI unit tests, real filesystem HTTP workflow, 8 formatter and 6 asynchronous runtime tests, and 2 doc-tests pass. Full workspace fmt/check/all-target tests/doc-tests, Nix clippy -D warnings, rustdoc -D warnings, public example and diff check pass. Scoped regressions cover same-name dispatch, stale/ref-less/replaced/deleted scopes, old in-flight contexts and selective typed-entry observation.
2026-10-08 04:03:51 +09:00
..

wip-tui

wip-tui is an interactive terminal client for exploring and operating a WIP Worldspace. It is a presentation and terminal-interaction layer over wip-client: Known Space, request identity, validators, immutable operation call contexts, stale transitions, response classification, and bounded histories remain owned by the client runtime.

Start

Supply one http or https endpoint:

cargo run -p wip-tui -- --endpoint https://wip.example.test/api/wip

The same canonical endpoint is used unchanged to construct requests, perform HTTP I/O, and partition the client cache. WIP over HTTP does not define separate HTTPS and development-HTTP profiles; URL scheme, TLS, certificate, authentication, and proxy policy belong to the HTTP implementation and deployment.

The endpoint may instead be supplied as WIP_ENDPOINT. A non-secret cache partition identity may be supplied with --security-context NAME or WIP_SECURITY_CONTEXT. The default request timeout is 30 seconds and can be changed with --timeout SECONDS.

An Authorization header can be read from an environment variable without placing its value in arguments, application state, errors, snapshots, or debug output:

export WIP_AUTHORIZATION='Bearer …'
cargo run -p wip-tui -- https://wip.example.test/wip

Use --authorization-env OTHER_VARIABLE to select another variable. The value is redacted from Debug and is never rendered. Shell and process environment security remain the operator's responsibility.

Filesystem server example

In one terminal:

mkdir -p /tmp/wip-example
printf 'hello world' >/tmp/wip-example/notes.txt
cargo run -p wip-fs-server -- \
  --root /tmp/wip-example \
  --listen 127.0.0.1:8080 \
  --base-path /wip \
  --read-write

In another:

cargo run -p wip-tui -- http://127.0.0.1:8080/wip

Expand /, select notes.txt, focus the Interface/Operation pane, and choose read; descriptors advertised by visible Objects are normally prefetched already. The editor labels offset and length as optional; leave both blank to read the bounded file. Bytes results are shown as a bounded hex preview. For replace, enter contents as RFC 4648 base64—for example, dXBkYXRlZA== for updated—then review the target Entry, interface reference, operation, and value on the confirmation screen before pressing Enter again.

Scoped reference formatting

wip_tui::reference::{format_interface_reference, format_qualified_operation} provides labels for the existing UI from InterfaceReference and a separate operation name, following 4.2-text-view.md reference notation (6086f3c).

use wip_protocol::InterfaceReference;
use wip_tui::reference::{format_interface_reference, format_qualified_operation};

let reference = InterfaceReference { scope: "/github".into(), name: "issue#read".into() };
assert_eq!(format_interface_reference(&reference)?, "/github::\"issue#read\"");
assert_eq!(format_qualified_operation(&reference, "read#raw")?,
           "/github::\"issue#read\"#\"read#raw\"");
# Ok::<(), wip_protocol::ValidationError>(())

Both return Result<String, ValidationError> and reject invalid reference shape, including relative/omitted scope. Root is explicit /. Non-simple components use JSON string quoting with additional DEL/C1, Unicode line/paragraph separator and bidi-control escapes. Input values are borrowed and never normalized, case-folded, URL-encoded or split on delimiters. Ref, validators and scope_ref do not enter these labels. The output has no extra outer signature string quoting.

Keep selection, cache and calls keyed by structured references and operation names, never display text. No parser, scope inference, identity reconstruction, Object/Interface signature renderer or new crate is introduced. Membership, ancestry, authorization and freshness remain runtime concerns. Normal Entry, Interface/Operation, editor, confirmation and operation-history screens use these qualified labels with existing bounded layout. Shortened labels never feed back into identity or calls.

Rust API and wire migration

Interface identities in the presentation map, action/selection rows, in-flight subjects and editors are now InterfaceReference { scope, name }, not strings. This is a breaking Rust API change; consumers must construct an explicit canonical absolute scope rather than parse a label or infer a scope from a target Entry. ArgumentEditor also retains its Entry path; new defaults to /, and for_target(path) fixes the chosen target before editing. Normal UI selection sets it automatically. Confirmation never retargets an existing editor. App::operation_selection retains the Entry and structured row through temporary loading gaps; the numeric cursor is only a layout position. Retaining this subject does not expose stale operations or authorize execution.

The TUI uses the scoped Protocol/HTTP Client wire: Interface references are structured, and calls use an Interface target carrying the reference and optional scope_ref/validator preconditions. The optional identity is copied by the Client, not invented by the UI; no additional required ref is introduced. This wire is not compatible with the former string-reference request/response shape.

Keyboard bindings

All required actions are keyboard-accessible.

Key Action
↑/k, ↓/j Move within the focused tree, operation list, history, or editor
→/l, Enter Expand a tree node lazily; load/select an interface or operation
←/h Collapse a node or move to its parent
Tab Cycle Tree → Selected Entry → Operations → History
r Reobserve the selected Entry and its direct subtree
c Cancel the newest retrieval; a dispatched operation is never presented as canceled
R Reconnect after connection failure
? Open help
Esc Close a modal or return to the tree
q, Ctrl-C Exit and restore the terminal

Inside an argument editor, printable keys are field input, arrows move fields, Backspace edits, Enter validates, and Esc cancels. The confirmation screen requires a second explicit Enter (or e) to execute once. There is no operation auto-retry.

Screen model

  • Worldspace tree starts at /, lazily materializes expanded nodes, and prefetches children for each visible Object so emptiness and the next expansion frontier are known without selecting that Object first. Text labels distinguish unloaded, loading, empty, stale, and failed state; color is supplemental. Every successful expansion is complete for its requested depth.
  • Selected Entry shows only public WIP path, object name, description, ordered interfaces, and freshness. Validators, credentials, Host internals, and backend paths are deliberately hidden. If current metadata disappears, old text is not presented as current.
  • Interfaces / Operations identifies every operation by structured (scope, interface name, operation name), displaying scope::interface#operation with explicit absolute scope and safely quoted components. Structured references remain intact in selection, acquisition, and call state; display labels are never parsed back into references. Descriptor lookahead follows fresh visible Objects, shares the client's in-flight/cache bounds, and never retries retained failures implicitly. The pane shows descriptor documentation, ordered parameters, required/optional status, and result types.
  • Argument editor supports Unit, Boolean, Integer, Number, String, base64 Bytes, Record, List, Entry, Enum, Union, Json, and descriptor-local named types. Record, List, Union, and Json values use JSON syntax. Union input is {"$case":"name","value":…} when the selected case has a payload. Diagnostics remain attached to the failing field.
  • Recent results / diagnostics explicitly distinguishes success, Host failure, invalid protocol response, transport failure, client-local failure, and unknown operation outcome. Large values, bytes, nesting, and control characters are bounded before rendering. Only descriptor-declared Entry results become navigation targets; path-looking String or Json values do not.

Terminals below 48×14 show a minimum-size screen instead of attempting the full layout.

Validator and failure behavior

Object and interface validators are observed and attached automatically by wip-client; users neither copy nor edit them. A successful operation may update the cached Object validator but does not by itself make the Object observation stale. Validator mismatch, interface mismatch, OperationOutcomeUnknown, undecodable post-dispatch responses, and post-dispatch timeout/disconnect are displayed using the runtime's authoritative transition.

Every Client transition is mirrored into retained UI Object, tree and Interface state, including scope retirement and cache removal. An open editor/confirmation is bound to private Object/Interface snapshots (not displayed refs/validators). Changed, stale, loading, removed or retargeted observations retire the editor; reobservation never recreates or executes it. Descriptor rows remain identified by the structured reference when another Interface gains or loses operation rows. Explicitly select/load an Interface header to refresh a retained stale descriptor, then select an operation and confirm a new call. Client calls already dispatched retain their original immutable context and are never moved to a new scope.

Without refs, a same-path replacement with indistinguishable public metadata cannot always be detected; the UI does not synthesize identity or infer deletion from missing tree edges. Optional refs remain optional. Typed Entry results are only observed when individually selected; the UI neither bulk-fetches result paths nor adds guessed parent/indexable edges.

For an unknown outcome, the UI states that execution or side effects may have occurred, marks the related Entry stale, and tells the user to refresh. Refreshing does not recreate the call: the user must select the operation, review fresh input, and explicitly execute again. The same no-auto-retry rule applies after validator or interface mismatch.

Retrieval work runs on I/O workers while the input/render loop continues. Responses return with the original RequestId, so an old completion cannot populate another Entry's details. Canceling a retrieval records a local cancellation and drops its late response. Once an operation crosses dispatch, cancellation is never described as undoing execution.

Tests

The library form keeps reducers and rendering independent of a real terminal. ratatui::TestBackend snapshots cover tree/detail/result states and the minimum-size fallback. Unit tests cover reducer focus, selection, modal confirmation, key translation, descriptor inputs, bounded values, and descriptor-directed Entry results. The real HTTP integration test launches wip-fs-server on a temporary root and covers tree selection, reads, validator-protected replacement, external stale-validator rejection without retry, explicit refresh/re-execution, and symlink confinement.