Extract hero_terminal_lib crate (PTY wire format, ANSI scrubbers, terminal envelope) #88
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_proc#88
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Background
Issue #71 added a design proposal at
docs/proposal-shared-terminal-lib.mdfor sharing terminal-related code betweenhero_procandhero_router. Discussion clarified the boundary:TerminalSession, CRUD,attached()registry,inject_*,terminal.*RPC). Stays put. Not extracted.This issue tracks the actual extraction.
Crate location
New crate
crates/hero_terminal_lib/inside the hero_proc workspace (not hero_router as the original proposal suggested). Rationale: hero_proc owns the PTY contract — the wire format and output post-processing should live with the source of truth.hero_routerconsumes it as a downstream dependency.This inverts the original proposal's direction; no follow-up CODEROOT plumbing is required because hero_router already depends on hero_proc (
hero_proc_sdkis already a workspace dep — seehero_router/Cargo.toml:24).Scope — three reusable modules
1.
ansi/— output scrubbing (no behavior change)Pure byte-level helpers for stripping the local-terminal CSI/DA query echoes from PTY output. Currently in
hero_router/src/server/terminal.rs:strip_local_terminal_queries(bytes: &mut Vec<u8>, rows: u16) -> Vec<Vec<u8>>(lines 222–311)strip_da_plaintext_artifacts(bytes: &mut Vec<u8>)(lines 312–338)pending_query_prefix_len(bytes: &[u8]) -> usize(lines 339–364)mod strip_tests(lines 365–481) — moves verbatimZero IO, zero async, zero axum. Pure functions over
&[u8]/&mut Vec<u8>.2.
wire/— hero_proc PTY WebSocket frame formatDefine the protocol that hero_proc_server speaks on
/api/jobs/{id}/ptyand/api/services/{name}/ptyas a typed enum, and ship encoders/decoders both ends share:Replaces:
{\"resize\":{\"cols\":N,\"rows\":M}}JSON parsing inhero_proc_server/src/web.rs::handle_pty_ws(line 358).hero_router/src/server/terminal.rs::pty_handler.After this lands, both ends call
PtyFrame::from_ws_*/to_ws_*instead of inlining JSON parsing. Wire bytes are unchanged — existing browsers connecting to either end keep working.3.
event/— typedTerminalEventenvelope (browser ↔ hero_router)The hero_router-side envelope used between the browser xterm.js code and the
pty_handlerproxy. Lives inhero_router/static/js/terminal.jstoday, parsed inline on the Rust side:Rust gains a typed enum + serde glue. The matching JS encoder/decoder in
terminal.jsis left alone in this issue — only the Rust side adopts the type.Useful when a second client (MCP shell tool, alternate UI) needs to speak the same envelope. If only hero_router ever uses it, this module is a small win at low cost; we get to keep the
Stdin/Stdoutlayer (PtyFrame) clean.Out of scope (explicitly)
TerminalSession,ShellType,encode_job_name/decode_job_name,validate_name,shell_suffix,attached(),inject_reply,inject_resize,create_session/list_sessions/get_session/delete_session, theterminal.*RPC handlers. All hero_router-specific. Stay there.PtyHandle,portable_ptysupervisor — stay inhero_proc_server).JobSummary,JobFilter, or backend RPC shape.Implementation plan
Step 1 — Create the crate
crates/hero_terminal_lib/in the hero_proc workspace; add to rootCargo.toml[workspace] members.serde,serde_json,base64,thiserror. Noaxum, notokio, notokio-tungstenite. Pure data + algorithms.lib.rsre-exporting the three modules.Step 2 — Move
ansi/(no behavior change)strip_local_terminal_queries,strip_da_plaintext_artifacts,pending_query_prefix_len, andmod strip_testsintohero_terminal_lib::ansi.hero_router/src/server/terminal.rs; adduse hero_terminal_lib::ansi::{strip_local_terminal_queries, strip_da_plaintext_artifacts, pending_query_prefix_len};.cargo test -p hero_terminal_libruns the movedstrip_tests.cargo build -p hero_routersucceeds with the new dep wired in.Step 3 — Define
wire::PtyFrameand adopt on both endshero_terminal_lib::wire::{PtyFrame, FrameError, Direction}with the encode/decode methods listed above.hero_proc_server/src/web.rs::handle_pty_ws(line 318): replace the inlineMessage::Binary/Message::Textarms withPtyFrame::from_ws_binary/from_ws_text. The match-on-PtyFramearms produce the samemaster.write/master.resizecalls as today.hero_router/src/server/terminal.rs::pty_handler: same swap, on the side that proxies bytes to/from hero_proc.hero_terminal_lib: round-trip every variant; reject malformed JSON; tolerate unknown JSON keys (forward-compat).Step 4 — Define
event::TerminalEventhero_terminal_lib::event::{TerminalEvent, EventError}withserdederives matching the existing JSON shape produced/consumed interminal.js.hero_router/src/server/terminal.rs::pty_handler: where it currently parses/builds the envelope inline, switch toserde_json::from_str::<TerminalEvent>/serde_json::to_string.Step 5 — Verification
cargo test --workspacegreen in both repos.hero_router: open the Terminal UI, start a session, type, resize, detach, reattach. No regression vs. pre-extraction behavior.hero_proc: open the Terminal tab on the admin dashboard (post-#71 with the TTY filter), attach to a TTY job, type, resize. No regression.Step 6 — Update the proposal doc
docs/proposal-shared-terminal-lib.mdgets a "Status: implemented in #" header and is shortened to a one-paragraph pointer to the crate's README.Acceptance criteria
crates/hero_terminal_lib/exists in the hero_proc workspace with the three modulesansi,wire,event.axum, zerotokio, zerotokio-tungstenitedependencies.hero_routerconsumes the crate via a normal git dep on hero_proc (added tohero_router/Cargo.toml [workspace.dependencies]).hero_router/src/server/terminal.rsno longer contains thestrip_*/pending_query_prefix_lendefinitions or thestrip_testsmodule — those use sites import fromhero_terminal_lib::ansi.hero_proc_server::web::handle_pty_wsparses inbound frames viaPtyFrame::from_ws_*(no inlineserde_json::from_strfor the resize shape).hero_router::server::terminal::pty_handleruses bothPtyFrame(for the hero_proc side) andTerminalEvent(for the browser side).cargo test --workspacepasses in both repos. Pre-existing failures from #71 (harness::tests::test_harness_starts_and_stops,ready::tests::declare_creates_marker, the 5 doc-test compile errors) are unchanged.docs/proposal-shared-terminal-lib.mdupdated to reflect the implemented state.Notes
hero_terminal_lib(matching the_libsuffix already used byhero_proc_lib).{\"v\":1,\"kind\":\"resize\",...}envelope), that's a follow-up — andPtyFramemakes it a single point to evolve.