Skip to content

feat(line-sink): attachLineSink + shared per-pane decoder — (i3m.2)#67

Merged
brandon-fryslie merged 1 commit into
masterfrom
tmux-pane-output-i3m.2
May 28, 2026
Merged

feat(line-sink): attachLineSink + shared per-pane decoder — (i3m.2)#67
brandon-fryslie merged 1 commit into
masterfrom
tmux-pane-output-i3m.2

Conversation

@brandon-fryslie

Copy link
Copy Markdown
Contributor

Summary

Builds on tmux-pane-output-i3m.1 (PR #59BytesSink + attachBytesSink substrate). 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 the TmuxClientLike attachBytesSink slice — works for TmuxClient, WebSocketTmuxClient, TmuxClientProxy, Electron renderer client without per-transport adapters.

Design

  • Per-client LineRegistry (module-private WeakMap<client, LineRegistry>) 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. The substrate guarantees SinkRegistry.dispatch passes the same reference to every admitting byte sink.
  • Each attachLineSink call registers its own BytesSink with its own scope, so admission is decided by SinkRegistry (one enforcer, no second admission table).
  • Buffer flushes when the last admitting consumer for a pane detaches.
  • CRLF sequences arrive with the trailing \r stripped — TTY output is bare text.

Laws engaged

  • [LAW:single-enforcer] — one decoder + buffer per pane; one dispatch path via the substrate's SinkRegistry.
  • [LAW:dataflow-not-control-flow] — pipeline 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 (11 cases) — cross-chunk lines, cross-chunk UTF-8 (4-byte emoji split mid-sequence), shared-decoder spy (TextDecoder.prototype.decode called 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 via send-keys without Enter), LINE-17 (no spurious flush), LINE-18 (detach-during-dispatch).

pnpm run test:all green — 34 files, 592 tests.

Test plan

  • pnpm run typecheck
  • pnpm run lint
  • pnpm run format:check
  • pnpm run check:deps
  • pnpm run test:all (unit + integration with real tmux)

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).
Copilot AI review requested due to automatic review settings May 28, 2026 07:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts with attachLineSink, LineEvent, LineHandler, using a module-private WeakMap<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.

@brandon-fryslie brandon-fryslie merged commit 599bcbb into master May 28, 2026
2 checks passed
@brandon-fryslie brandon-fryslie deleted the tmux-pane-output-i3m.2 branch May 28, 2026 07:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants