Commit 486294b
Vim/hyp 184 cli hs follow command for websocket streams (#74)
* feat: add `hs stream` command for live WebSocket entity streaming (Phase 1)
Core streaming MVP: connect to a deployed stack's WebSocket, subscribe
to a view (e.g. OreRound/latest), and stream entity data as NDJSON to
stdout. Supports --raw mode for raw frames and merged entity output
(default). Resolves WebSocket URL from --url, --stack, or hyperstack.toml.
Also exports parse_frame, parse_snapshot_entities, ClientMessage, and
deep_merge_with_append from hyperstack-sdk for reuse.
* feat: add filtering, --first, --select, --ops, --no-dna, --count to hs stream (Phase 2)
- Filter DSL via --where: =, !=, >, >=, <, <=, ~regex, !~regex, ?, !?
with dot-path support for nested fields (e.g. --where "user.age>18")
- --first exits after first entity matches filter criteria
- --select projects specific fields (comma-separated dot paths)
- --ops filters by operation type (upsert, patch, delete)
- --no-dna outputs NO_DNA v1 agent-friendly envelope format with
lifecycle events (connected, snapshot_complete, entity_update, disconnected)
- --count shows running update count on stderr
- 9 unit tests for filter parsing and evaluation
* feat: add --save/--load snapshot recording and replay to hs stream (Phase 3)
- --save <file> records all raw frames with timestamps to a JSON file
- --duration <secs> auto-stops recording after N seconds
- --load <file> replays a saved snapshot through the same merge/filter
pipeline (no WebSocket connection needed)
- Snapshot format includes metadata (view, url, captured_at, duration)
* feat: add EntityStore with history, --history, --at, --diff flags (Phase 4)
EntityStore tracks full entity state + per-entity history ring buffer
(default 1000 entries). Supports:
- --history: outputs full update history for --key entity as JSON array
- --at N: shows entity state at specific history index (0 = latest)
- --diff: shows field-level diff (added/changed/removed) between updates
with raw patch data when available
These flags provide non-interactive agent equivalents of the TUI time
travel feature. 5 unit tests for store operations and diffing.
* feat: add interactive TUI mode for hs stream (Phase 5)
Behind --features tui flag, `hs stream --tui` launches a ratatui-based
terminal UI with:
- Split-pane layout: entity list (30%) + detail view (70%)
- Entity navigation (j/k, arrows), detail focus (Enter/Esc)
- Time travel through entity history (h/l, Home/End)
- Diff view toggle (d) showing field-level changes
- JSON syntax coloring in detail panel
- Pause/resume live updates (p)
- Save snapshot to file (s)
- Entity key filtering (/)
- Raw frame toggle (r)
- Status bar with keybinding hints
- Timeline bar showing history position
Dependencies: ratatui 0.29, crossterm 0.28 (optional)
Without the tui feature, --tui prints an error with install instructions.
* fix: properly handle subscribed frames on binary WebSocket channel
The server sends subscribed acknowledgments as binary frames with a
different shape (no `entity` field), causing parse_frame to fail.
Now falls back to try_parse_subscribed_frame before warning, so real
parse errors are still surfaced while subscribed frames are handled
cleanly. Re-exports try_parse_subscribed_frame from hyperstack-sdk.
* fix: route all keypresses to filter input when filter mode is active
Previously, keys like r, d, s, h, l etc. were matched as TUI commands
before checking if filter input was active, making it impossible to
type those characters in the filter. Now checks filter_input_active
first and routes all Char keys to the filter text input.
* feat: TUI filter searches across all entity data values, not just keys
The / filter now recursively searches all string, number, and boolean
values in each entity's JSON data. Typing "test" matches any entity
where any field value contains "test" (case-insensitive), not just
entities whose key contains the search term.
* fix: clamp selected index to filtered list when filter text changes
When typing a filter that reduces the entity list, the selection now
clamps to stay within the filtered results. Navigation (j/k) also
bounds against the filtered count instead of the full entity list.
* fix: auto-scroll entity list to keep selected item visible
Uses ratatui's ListState with the selected index so the list widget
automatically scrolls to keep the highlighted entity in view when
navigating past the visible area. Also shows filter text and
filtered/total count in the title when a filter is active.
* feat: add vim motion commands to TUI
- gg: jump to top of list
- G: jump to bottom of list
- Ctrl+d / Ctrl+u: half-page down/up
- n: jump to next filter match (wraps around)
- Number prefixes: e.g. 10j moves down 10, 5k moves up 5,
3Ctrl+d moves 3 half-pages down
- Esc clears any pending count/g prefix
* fix: persist ListState so scrolling works in both directions
ListState was being recreated fresh each frame, losing the scroll
offset. Now stored in App and synced with selected_index on every
action, so ratatui properly auto-scrolls the entity list in both
directions — pressing k after scrolling past the bottom now scrolls
back up as expected.
* fix: disambiguate list position from entity version in timeline bar
The timeline bar now shows two distinct pieces of info:
- Row position: "Row 1/513" (your position in the entity list)
- Entity version: "version 1/2" (update history for the selected entity)
Previously only showed "update N/N" which was confusing because it
looked like it referred to list position rather than entity history.
* fix: remove duplicate entity count from header
The entity count is already shown in the list panel title, no need
to repeat it in the header bar.
* chore: Remove plan
* fix: move --duration deadline into tokio::select! arm for precise timing
Previously the deadline was checked at the top of the loop before
entering select!, so on quiet streams the actual stop time could
overshoot by up to the 30-second ping interval. Now uses a sleep
future as a select! arm that fires exactly when the duration expires.
* fix: use full dot-path as key in --select to prevent data loss on collisions
Previously --select "a.id,b.id" would output {"id": <b's value>},
silently overwriting a.id. Now uses the full dot-path as the output
key: {"a.id": 1, "b.id": 2}. Single-segment paths are unchanged
(--select "name" still outputs {"name": ...}).
Adds test for the collision case.
* fix: make TUI raw-frame toggle (r) actually render raw frames
raw_frames were only collected when show_raw was active, and were
never read by any rendering code. Now:
- Always collects raw frames (so toggling on shows recent data)
- selected_entity_data() checks show_raw first and returns the most
recent raw WebSocket frame for the selected entity key
* fix: add panic hook to restore terminal on TUI crash
If the TUI panics, disable_raw_mode and LeaveAlternateScreen never
executed, leaving the user's terminal in raw mode (unusable until
running 'reset'). Now installs a panic hook before entering raw mode
that restores the terminal state before re-invoking the original hook.
* perf: use HashSet for O(1) entity key deduplication in TUI
entity_keys.contains() was O(n) per frame, degrading with thousands
of entities. Now maintains a parallel HashSet<String> for O(1)
membership checks. HashSet::insert returns false if already present,
so it doubles as the contains check. Delete also removes from the set.
* fix: resolve clippy warnings in filter module
- Use strip_suffix instead of manual string slicing for ? and !? suffix
- Use is_some_and/is_none_or instead of map_or for Option comparisons
- Use direct == Some() comparison instead of map_or for equality check
* fix: emit snapshot_complete NO_DNA event for binary WebSocket frames
The snapshot_complete detection and NO_DNA event emission only existed
in the Message::Text branch. Since the server primarily sends binary
frames, consumers relying on the NO_DNA snapshot_complete lifecycle
event would never see it. Now mirrors the same tracking logic in the
binary frame branch.
* fix: prevent panic on multi-byte UTF-8 entity keys in truncate_key
Byte-index slicing panics when the cut point lands in the middle of a
multi-byte codepoint (emoji, CJK characters). Now uses char_indices
to find safe byte boundaries for truncation.
* fix: prevent silent connection to wrong stack when multiple stacks configured
Previously fell through to the first stack with any URL, silently
connecting to an unrelated stack. Now only auto-selects when there is
exactly one stack with a URL (unambiguous), and prints which stack was
chosen. With multiple stacks, requires explicit --url or --stack.
* fix: --first without --where now exits after first frame/entity
Previously --first only triggered when a --where filter was present,
silently running forever without one. Now --first always exits after
the first output: with --where it exits on first match, without
--where it exits after the first frame (raw) or entity (merged).
* docs: clarify operator parsing order and value splitting in filter DSL
Adds comments explaining that two-char operators are checked before
single-char to avoid misparsing, and that the split is on the first
operator occurrence so values may contain operator characters
(e.g. --where "name=a=b" works correctly).
* fix: snapshot_complete NO_DNA event tracks received frames, not filtered count
Previously used update_count > 0 to detect snapshot completion, which
failed when all snapshot entities were filtered out or --no-snapshot
was used. Now tracks whether any snapshot frame was received, so the
event fires correctly after the last snapshot frame regardless of
filter results.
* fix: reject --load --tui with clear error, replace process::exit with bail!
--load --tui silently ignored the TUI flag and fell back to
non-interactive replay. Now errors with a clear message.
Also replaces std::process::exit(1) for missing VIEW with bail!() so
errors propagate properly and destructors run.
* fix: remove always-on SnapshotRecorder to prevent unbounded memory growth
TUI unconditionally allocated a SnapshotRecorder that stored every
frame without any cap, causing unbounded memory growth in long-running
sessions. Now builds the recorder on-demand from the existing
raw_frames ring buffer when the user presses 's' to save.
* fix: --duration help text accurately reflects it stops the stream
Help said "Auto-stop recording after N seconds (used with --save)"
but --duration actually terminates the entire stream, not just
recording. Updated to "Auto-stop the stream after N seconds".
* fix: surface deserialization error when loading corrupt snapshot files
Previously swallowed frame deserialization errors with .ok() and
returned an empty Vec, showing "0 frames" with no indication of
failure. Now propagates the error with context so users see what
went wrong (e.g. schema mismatch, partial corruption).
* fix: apply --where filter and --first to Delete operations
Delete events bypassed the filter predicate and --first check,
always emitting regardless of whether the deleted entity matched.
Now filters against the entity's last-known state before removal
and respects --first.
* fix: emit snapshot_complete NO_DNA event when --no-snapshot is used
When --no-snapshot was set, no snapshot frames arrived so
received_snapshot stayed false and snapshot_complete was never
emitted. Now initializes received_snapshot to true when --no-snapshot
is active, so the first live frame triggers the lifecycle event.
* fix: TUI snapshot save preserves frame timing, raw mode handles snapshot entities
Previously TUI save created a fresh SnapshotRecorder at save time,
so all frames got ts=0. Now stores arrival Instant alongside each
frame and uses record_with_ts to preserve actual relative timing.
Also fixes raw mode showing nothing for snapshot-ingested entities:
falls back to showing the merged state with a note explaining the
entity was received via batch snapshot.
* fix: delay first ping tick by 30s instead of firing immediately
tokio::time::interval fires its first tick at t=0, sending a Ping
immediately after subscribing. Now uses interval_at to delay the
first tick by the full 30s period. Fixed in both client.rs and
tui/mod.rs.
* fix: gate record_with_ts behind tui feature to avoid dead_code warning
Only used from the TUI module, so compiling without --features tui
flagged it as dead code.
* fix: normalize op_str to lowercase in --ops filter comparison
The allowed_ops set is lowercase but the frame op string was compared
as-is, so mixed-case ops from the server would be silently dropped.
* fix: address final review feedback (5 items)
- !~ (NotRegex) filter now returns false for missing fields, consistent
with all other negative operators
- snapshot_complete NO_DNA event emitted before disconnected when
--first exits during snapshot phase
- Extract shared build_subscription() helper, removing duplicated
subscription-building logic between client.rs and tui/mod.rs
- TUI sends graceful WebSocket Close frame via oneshot shutdown signal
instead of aborting the task
- print_count flushes stderr so \r overwrite works in piped contexts
* fix: emit snapshot_complete in replay NoDna mode
replay() emitted connected and disconnected but never
snapshot_complete, breaking the NO_DNA v1 lifecycle contract
for agents consuming --load --no-dna output.
* fix: disable raw mode on terminal setup failure
If execute!(EnterAlternateScreen) or Terminal::new failed after
enable_raw_mode(), the function returned early via ? without
restoring the terminal. Now catches setup errors and calls
disable_raw_mode() before propagating.
* fix: stop dropping frames while TUI is paused
Previously frames were drained from the channel and discarded while
paused, permanently losing updates. Now leaves frames in the channel
when paused so they're applied on resume, preserving entity state
continuity.
* fix: include snapshot operations in --ops filter
Snapshot entities previously bypassed the --ops filter entirely, so
--ops patch would still emit all snapshot entities. Now treats
snapshot frames as op type "snapshot" in the filter check, so
--ops patch only shows patches and --ops snapshot,upsert shows both.
* fix: use derived Deserialize for SnapshotHeader instead of manual parsing
Manual field-by-field extraction silently defaulted to empty/zero on
missing fields and would miss new fields without compile-time warning.
Now uses serde_json::from_value with the derived Deserialize impl,
which propagates errors on invalid/missing fields.
* fix: TUI update_count increments per entity, not per snapshot frame
A snapshot frame containing 500 entities incremented update_count by
1 instead of 500, making the TUI header inconsistent with the CLI
output. Now counts each entity individually.
* docs: note that field? treats null as absent in --where help text
The Exists operator returns false for JSON null values, which may
surprise users who distinguish between missing keys and explicit
nulls. Added a note to the help text.
* feat: add detail pane scrolling with Ctrl+e/Ctrl+y and PageDown/PageUp
scroll_offset was never incremented, making the detail pane stuck at
the top. Large entity JSON was truncated with no way to see the rest.
Now Ctrl+e/Ctrl+y scroll line-by-line (with count prefix support)
and PageDown/PageUp also work.
* fix: --ops filter suppresses output but always populates entity state
Previously --ops without "snapshot" dropped snapshot frames entirely,
so later patch merges ran against empty {} state producing incorrect
output. Now always processes frames for state tracking but only emits
output when the op type passes the filter. This ensures patches merge
correctly against snapshot-populated entity state.
* docs: document --raw --where limitation for snapshot batch frames
In raw mode, --where filters against the raw frame.data which is a
JSON array for snapshot frames. Field-level predicates won't match
snapshot batches. Added a comment explaining the trade-off.
* fix: validate snapshot version on load
Previously ignored the version field, so future format changes would
cause silent or confusing deserialization failures. Now rejects
unsupported versions with a clear error message.
* fix: clear pending_count on g prefix, add resize handling comment
- Clears pending_count when g sets pending_g, preventing a spurious
count from being consumed if the user presses a non-g key between
the two g presses
- Adds comment documenting that resize events are handled implicitly
via terminal.size() recalculation at the top of each loop iteration
- Collapses nested if statements per clippy
* fix: always attempt all terminal cleanup steps on TUI exit
Used ? which would short-circuit if disable_raw_mode failed, skipping
LeaveAlternateScreen and show_cursor. Now uses let _ = to ignore
errors and attempt every cleanup step regardless.
* fix: snapshot_complete fallback guard uses received_snapshot not update_count
When --ops excludes snapshots, update_count stays 0 despite snapshots
being received (for state tracking). The fallback guard now checks
received_snapshot so the NO_DNA lifecycle event fires correctly.
* fix: warn when snapshot file has no 'frames' key
Previously returned an empty player silently, showing "0 frames"
with no indication the file was malformed. Now prints a warning.
* fix: validate WebSocket URL scheme before connecting
Passing http:// or https:// instead of ws://wss:// produced a
cryptic error from tokio-tungstenite. Now validates the scheme
upfront with a clear error message.
* fix: warn when --diff and --history are combined
--diff takes precedence and silently ignores --history. Now prints
a warning so the user knows to remove --diff to see full history.
* fix: emit snapshot_complete before first live frame in NO_DNA mode
Previously snapshot_complete was emitted after process_frame, so the
first live entity_update appeared before the snapshot_complete event,
violating NO_DNA lifecycle ordering. Now emits the transition event
before processing the first live frame. Also fixes --no-snapshot
emitting entity_count:1 instead of 0.
* fix: reject TUI-incompatible flags with clear error messages
--duration, --count, --save, --history, --at, and --diff were silently
ignored in --tui mode. Now bail with messages pointing to TUI equivalents.
* fix: deserialize snapshot in single pass to avoid cloning entire JSON
Previously cloned the full serde_json::Value (including all frames)
just to parse the small header. Now uses a combined SnapshotFile struct
with #[serde(flatten)] for one-pass deserialization.
* fix: NotRegex returns true for absent fields, consistent with NotEq
Previously !~ returned false when the field was missing, while !=
returned true — inconsistent semantics. Now both negative operators
treat absent fields as "does not match", which is the intuitive
behavior for exclusion filters.
* fix: use VecDeque for raw_frames to avoid O(n) drain from front
Vec::drain(0..500) shifts all remaining elements. VecDeque gives O(1)
pop_front and maintains exactly 1000 entries instead of oscillating.
* fix: remove mouse capture from TUI to restore terminal copy-paste
Mouse capture was enabled but mouse events were never handled, only
preventing users from selecting/copying text in their terminal.
* refactor: extract snapshot_complete emission into helper function
The snapshot_complete NoDna check was duplicated verbatim in both the
binary and text WebSocket message handlers. Now uses a shared
maybe_emit_snapshot_complete() function.
* fix: TUI deep search also matches JSON field names, not just values
Typing /symbol now finds entities with a "symbol" field, not just
entities where "symbol" appears as a value.
* fix: simplify snapshot empty-frames warning (both branches were identical)
* fix: use try_send for TUI frame channel to prevent blocking when paused
When paused, frames accumulate in the bounded channel. The blocking
send() prevented pings and shutdown from firing. Now uses try_send()
which drops frames when the channel is full (acceptable while paused).
* fix: reset history_position and scroll_offset on entity delete
Stale history_position could cause the detail pane to show nothing
after a delete shifted the selected entity.
* fix: replay emits snapshot_complete at correct boundary, not after all frames
Previously replay() emitted snapshot_complete unconditionally after the
entire loop. Now uses maybe_emit_snapshot_complete() to detect the
snapshot-to-live transition, matching the live stream behavior.
* fix: warn eagerly at startup when --history/--at/--diff lack --key
Previously the warning was only shown at stream end. Now warns
immediately so users know the store is allocated but won't produce
output without --key.
* fix: show dropped frame count in TUI header when channel is full
When paused, try_send drops frames that exceed the 1000-frame channel.
Now tracks the count via an atomic counter and shows a red "Dropped: N"
indicator in the header so users know data was lost.
* fix: use checked arithmetic for history index to prevent overflow
1 + index and len - 1 - index could overflow on usize::MAX.
Now uses checked_add/checked_sub returning None safely.
* fix: restore default panic hook after TUI exits
The custom panic hook (for terminal restoration) was left installed
after run_tui returned. Now drops it on exit so it doesn't interfere
with any subsequent code.
* perf: cache filtered_keys result, invalidate on filter change or new frame
filtered_keys() did O(n*m) deep JSON scans on every render frame
(50ms tick) and was called multiple times per frame. Now caches the
result and only recomputes when filter text changes or frames arrive.
* test: add coverage for !=, >=, <=, !~ operators and two-char precedence
5 new tests covering the four previously untested operators and
verifying two-char operators take precedence over single-char prefixes.
* fix: always attempt all terminal cleanup steps on TUI exit
- LeaveAlternateScreen now runs on Terminal::new failure
- Original panic hook properly restored via Arc<Mutex> on normal exit
(previously lost when moved into the closure)
* fix: validate WebSocket URL scheme for all resolved URLs, not just --url
URLs from hyperstack.toml (via --stack or auto-match) were not
validated, producing cryptic tokio-tungstenite errors on http:// URLs.
Extracted validate_ws_url() helper applied to all code paths.
* fix: debug_assert on filtered_keys() to catch missing ensure_filtered_cache
Prevents silent empty results if a future caller forgets to ensure
the cache. Falls back gracefully in release builds.
* fix: snapshot duration from frame timestamps, normalize create→upsert in --ops
- TUI snapshot duration_ms now computed from first/last frame ts instead
of recorder start_time which was near-zero
- --ops upsert now also matches create frames (semantically identical)
- Document panic hook limitation and history() ordering convention
* fix: pending_g, delete history retention, snapshot cap, entity_count
- Reset pending_g after GotoTop (vim gg required double-press again)
- Delete retains entity history with tombstone for post-stream analysis
- SnapshotRecorder capped at 100k frames with warning
- entity_count updated after snapshot loop, not per-iteration
* fix: ignore ctrl/alt combos in filter input, add Ctrl+U/Ctrl+W
Ctrl+D, Ctrl+U etc were being inserted as literal characters in the
filter text. Now Ctrl+U clears the filter line, Ctrl+W deletes the
last word, and other control/alt combos are ignored.
* fix: ops create normalization, entity_count timing, atomic snapshot write
- --ops create now correctly matches Create frames (normalized to upsert)
- entity_count updated per-entity in snapshot loop so NoDna events
have accurate counts
- record_with_ts respects MAX_FRAMES cap
- --load conflicts_with tui at Clap level (cleaner error)
- Snapshot writes use tmp+rename for atomicity
* fix: subscribed frame fallback, atomic write, early validation, filter errors
- TUI WS task handles subscribed frames via try_parse_subscribed_frame
fallback (previously silently dropped)
- Snapshot tmp file placed in same directory as target (avoids EXDEV)
- build_state called before connect_async for fast-fail on bad args
- Empty field name in --where (e.g. "=value") now returns clear error
* fix: float equality, visible_rows off-by-one, dead code, filename collisions
- Float equality uses string match first, then epsilon comparison
(avoids f64 rounding issues for decimals like 1.1)
- visible_rows subtracts 5 not 6 (header+timeline+status+2 borders)
- Remove unreachable --load/--tui runtime check (clap handles it)
- Snapshot filenames include milliseconds to prevent same-second collisions
* fix: snapshot recorder limit warning only prints once
The warning fired on every call after MAX_FRAMES since len never
exceeded the cap. Now uses a limit_warned flag to print once.
* fix: selection drift on delete, reject --raw/--no-dna with --tui, remove premature log
- Delete of entity before cursor now shifts selected_index back to
preserve which entity the user was looking at
- --raw and --no-dna with --tui now bail with clear messages
- Removed premature "Subscribing to..." printed before connection
* fix: NoDna connected event emitted after connect, replay tagged with source
- Moved connected event from build_state to after connect_async succeeds,
so failed connections don't emit a connected event with no matching
disconnected
- Replay connected event includes "source": "replay" so consumers can
distinguish live vs replay
- --load now conflicts_with --duration at clap level
* fix: TUI filter flag rejection, disconnect detection, float eq, WS close
- Bail on --where/--select/--ops/--first with --tui (previously ignored)
- TUI detects WebSocket disconnect and shows DISCONNECTED in header
- Float equality uses exact bitwise comparison after string match
(relative epsilon was too loose for large numbers)
- Duration expiry sends WebSocket close frame before breaking
* fix: count line cleanup, Windows rename, document snapshot entity_count
- finalize_count() clears the overwriting \r count line before post-
stream messages (prevents garbled terminal output)
- Snapshot write removes existing destination before rename (Windows
compatibility where fs::rename fails if target exists)
- Document that NoDna snapshot entity_count is a running tally
- Document silent delete filter drop for unseen entities
* fix: scope Windows rename workaround with cfg, clean up tmp on failure
- Remove-before-rename only runs on Windows (POSIX rename overwrites)
- Propagate remove_file errors instead of silently swallowing them
- Clean up tmp file on rename failure before propagating error
* perf: shared StdoutWriter, single-parse text frames, dedup diff_at lookups
- Output functions now use a shared BufWriter<Stdout> held in StreamState
instead of acquiring/releasing stdout lock per call
- Text WebSocket frames parsed once directly to Frame instead of double-
parsing (Value then Frame)
- diff_at stores entry in local variable instead of 3 redundant gets
- TUI channel buffer increased to 10k for large snapshot batches
- Document that filter cache invalidation is per-tick not per-frame
* fix: replay snapshot_complete guard, docs for select/diff/colorize, remove dead flush
- Replay snapshot_complete now checks received_snapshot (consistent
with live stream path)
- Document --select flattening behavior in help text
- Document compute_diff as shallow top-level only
- Document colorize_json_line serde_json assumption
- Confirm --first semantics with comment
- Remove unused StdoutWriter::flush (Drop impl suffices)
* fix: flush stdout per write, streaming snapshot, key coloring, pending_count cap
- StdoutWriter flushes after each writeln (prevents delays on low-
throughput streams)
- JSON key coloring uses "\": " to avoid matching colons inside keys
- Snapshot serialization streams to file via BufWriter instead of
building full JSON string in memory
- Document --ops snapshot as valid value
- Cap pending_count at 99999 to prevent usize overflow
* feat: context-aware j/k navigation in TUI detail mode
In Detail mode (after pressing Enter), j/k now scroll the JSON
detail pane instead of navigating between entities. G/gg go to
bottom/top of the JSON, Ctrl+d/Ctrl+u do half-page scroll.
Arrow keys still navigate entities in both modes as an escape hatch.
Press Esc to return to list mode where j/k navigate entities.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: stable history position during live streaming in TUI
Previously history_position was relative (0=latest, N=Nth from latest),
so when new frames arrived for the selected entity, the viewed content
would jump because the same relative index now pointed to a different
history entry.
Now stores an absolute VecDeque index (history_anchor) when browsing
history. The anchor stays fixed as new entries are appended. When the
ring buffer evicts old entries (pop_front), the anchor is decremented
to continue pointing at the same entry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: compact inline array rendering in TUI detail pane
Small arrays like [1, 2, 3] now render on a single line when they
fit within the terminal width, instead of one element per line.
Larger arrays that exceed the width still expand to multi-line.
Uses a custom JSON formatter (compact_pretty) that tries the inline
form for each array and falls back to expanded when it doesn't fit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: dynamic entity sorting in TUI with S/O key bindings
Press S to cycle sort modes (insertion order → _seq field → insertion).
Press O to toggle ascending/descending direction.
Sort applies to the filtered cache after filtering, never mutates the
raw entity_keys list. Numbers sort numerically, strings lexicographically,
null/missing values sort last.
Sort indicator shown in status bar: [_seq↓] or [_seq↑].
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: clamp detail scroll to content height, show line indicator
- G now scrolls to the last line of JSON, not past it
- All scroll actions clamp to max_scroll_offset (total lines - visible)
- Detail mode border is yellow to indicate focus state
- Scroll position shown in title: [line N/total]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: swap sort/save key bindings for ergonomics
- s = cycle sort mode (was S)
- o = toggle sort direction (was O)
- S = save snapshot (was s)
Lowercase for frequent actions, uppercase for the less common save.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: VimMotions <220220743+vimmotions@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>1 parent d7f5291 commit 486294b
15 files changed
Lines changed: 3594 additions & 15 deletions
File tree
- cli
- src
- commands
- stream
- tui
- rust/hyperstack-sdk/src
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
| 23 | + | |
22 | 24 | | |
23 | 25 | | |
24 | 26 | | |
| |||
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
| 38 | + | |
36 | 39 | | |
37 | 40 | | |
38 | 41 | | |
| |||
41 | 44 | | |
42 | 45 | | |
43 | 46 | | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
44 | 52 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
0 commit comments