feat(line-sink): attachLineSink + shared per-pane decoder — (i3m.2)#67
Merged
Conversation
The line-shaped consumer behavior built on top of attachBytesSink (.1).
attachLineSink(client, onLine, options?) is a free function over the
TmuxClientLike attachBytesSink slice — works for TmuxClient,
WebSocketTmuxClient, TmuxClientProxy without per-transport adapters.
Per-client LineRegistry (module-private WeakMap) owns per-pane state:
streaming TextDecoder + partial-line buffer + admitting-consumer set.
N line consumers for the same pane share one decoder; chunk decode runs
exactly once per arrival because PaneOutputMessage object identity is the
"already decoded this chunk" token (SinkRegistry.dispatch passes the same
reference to every admitting byte sink — substrate guarantee).
Buffer flushes when the last admitting consumer for a pane detaches; CRLF
sequences arrive with the trailing \r stripped.
[LAW:single-enforcer] one decoder + buffer per pane, one dispatch path
via the substrate's SinkRegistry.
[LAW:dataflow-not-control-flow] lifecycle is admitting-set membership,
not guarded branches.
[LAW:make-it-impossible] LineEvent carries only { line, paneId } — the
decoder is unreachable from consumer-held values.
Tests:
- tests/unit/line-sink.test.ts — cross-chunk + cross-pane + shared
decoder + CRLF + detach-during-dispatch (11 cases, deterministic
chunk boundaries via a fake client)
- tests/integration/line-sink.test.ts — LINE-10/13/14/14b/15/16/17/18
against an ephemeral tmux server (8 cases)
pnpm run test:all green (34 files, 592 tests).
There was a problem hiding this comment.
Pull request overview
Adds attachLineSink(client, onLine, options?) — a free function over the attachBytesSink slice that delivers UTF-8 lines per pane, with a shared TextDecoder and partial-line buffer per pane so N line consumers on the same pane decode exactly once per chunk. Builds on the BytesSink/PaneScope substrate from PR #59.
Changes:
- New
src/line-sink.tswithattachLineSink,LineEvent,LineHandler, using a module-privateWeakMap<client, LineRegistry>for per-pane decoder/buffer state and admitting-consumer tracking; CRLF stripping; tail flush when the last admitting consumer for a pane detaches. - Exports added in
src/index.ts. - Unit tests covering cross-chunk lines, split UTF-8, shared-decoder identity (spy on
TextDecoder.prototype.decode), per-pane isolation, CRLF stripping, detach-during-dispatch, partial-buffer survival; integration tests against ephemeral tmux for paneScope/sessionScope/windowScope/serverScope, multi-consumer parity, partial-tail flush, and detach-during-dispatch.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/line-sink.ts | New module implementing attachLineSink and per-pane shared-decoder line registry. |
| src/index.ts | Re-exports attachLineSink, LineEvent, LineHandler. |
| tests/unit/line-sink.test.ts | Unit tests exercising cross-chunk decode, shared decoder, scope routing, flush/detach semantics. |
| tests/integration/line-sink.test.ts | Integration tests against ephemeral tmux for scope routing and detach behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Builds on
tmux-pane-output-i3m.1(PR #59 —BytesSink+attachBytesSinksubstrate). Adds the line-shaped consumer behavior — the default text path most consumers (matchers, loggers, search, AI capture, assertions) will reach for.attachLineSink(client, onLine, options?)is a free function over theTmuxClientLikeattachBytesSinkslice — works forTmuxClient,WebSocketTmuxClient,TmuxClientProxy, Electron renderer client without per-transport adapters.Design
LineRegistry(module-privateWeakMap<client, LineRegistry>) owns per-pane state: streamingTextDecoder+ partial-line buffer + admitting-consumer set.PaneOutputMessageobject identity is the "already decoded this chunk" token. The substrate guaranteesSinkRegistry.dispatchpasses the same reference to every admitting byte sink.attachLineSinkcall registers its ownBytesSinkwith its own scope, so admission is decided bySinkRegistry(one enforcer, no second admission table).\rstripped — TTY output is bare text.Laws engaged
[LAW:single-enforcer]— one decoder + buffer per pane; one dispatch path via the substrate'sSinkRegistry.[LAW:dataflow-not-control-flow]— pipeline lifecycle is admitting-set membership, not guarded branches.[LAW:make-it-impossible]—LineEventcarries only{ line, paneId }; the decoder is unreachable from consumer-held values.Tests
tests/unit/line-sink.test.ts(11 cases) — cross-chunk lines, cross-chunk UTF-8 (4-byte emoji split mid-sequence), shared-decoder spy (TextDecoder.prototype.decodecalled exactly once per chunk), per-pane decoder isolation, CRLF stripping, detach-during-dispatch snapshot protection, partial-buffer survival across consumers.tests/integration/line-sink.test.ts(8 cases, ephemeral tmux) — LINE-10 (paneScope lines), LINE-13 (multi-consumer parity), LINE-14 (sessionScope), LINE-14b (windowScope isolation), LINE-15 (serverScope + paneId tagging), LINE-16 (partial-tail flush viasend-keyswithoutEnter), LINE-17 (no spurious flush), LINE-18 (detach-during-dispatch).pnpm run test:allgreen — 34 files, 592 tests.Test plan
pnpm run typecheckpnpm run lintpnpm run format:checkpnpm run check:depspnpm run test:all(unit + integration with real tmux)