Closes#5052
Multi-line input is currently sent as one PRIVMSG per line, so a paste
arrives as N unrelated messages. On networks that support
[draft/multiline](https://ircv3.net/specs/extensions/multiline) it can
be sent as a single BATCH and stays one message everywhere.
## Summary
Sending goes through `sayMultiline()` in `plugins/inputs/msg.ts` when
the network advertises limits we can respect (`multilineLimits()`); if
the batch is over max-bytes/max-lines the send falls back to individual
messages, and the local echo follows whichever path was actually taken
so the sender sees what everyone else got. `Client.input()` stops
splitting on newlines when the target network has the cap. Inbound
batches arrive from irc-framework already reassembled and are flagged
`multiline` on the `Msg`, which drives line-by-line rendering (`parse()`
joins lines with `<br>`), one entry per message in the text logs, and a
first-non-empty-line notification preview. `MULTILINE_*` standard
replies get readable text.
No pin bump: `master` already points at the irc-framework commit that
carries multiline (kiwiirc/irc-framework#414).
## Test Plan
- New unit tests for the send path (batch, fallback on max-bytes, no
limits advertised, blank lines), the text log format, `parse()`, and the
standard replies.
- Ran the server against a local IRCd advertising `draft/multiline` and
confirmed on the wire: a 3-line message goes out as one `BATCH …
draft/multiline` with three frames; with the cap advertised bare (no
limits) or with `max-bytes` too small it degrades to three plain
PRIVMSGs and the local echo degrades with it; a CRLF paste sends clean
frames and echoes without stray `\r`; an inbound batch from another user
renders as one message.
- Not verified: rendering was checked through unit tests rather than in
a browser.
Relies on https://github.com/kiwiirc/irc-framework/pull/411https://ircv3.net/specs/client-tags/reply.html
Design feedback very welcome
<img width="1718" height="278" alt="Screenshot 2026-04-11 at 21-00-22
#thelounge — The Lounge"
src="https://github.com/user-attachments/assets/ba6e7c69-2477-4a7b-b196-901d19ca9cba"
/>
1. we lack a 'thread' view (irccloud, slack, etc). can be a followup if
we do want it
2. need to test scrolling to messages outside of the current buffer
3. due to lack of client support, i opted to auto prefill the input with
\`<targetNick>: \` so people can follow along even without reply support
4. CSS can definitely be improved (once a design is settled on)
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Found by claude:
```
server/plugins/inputs/mute.ts — /mute slash command never persists. Missing client.save(). Mutes set via the command vanish on restart. The socket handler (server/server.ts:768-797) does save.
server/plugins/inputs/mute.ts:18 — /mute #Foo fails when channel was joined as #foo. Uses case-sensitive c.name === arg; IRC channel names are case-insensitive.
```
- Mentions were doing a bunch of mutations; not just uses derived state
- no need for a separate `ClientMention`
- added `extractInputHistory` for shared logic
- tests made by claude
We should not mess with irc-framework internals.
Technically we shouldn't even access the connection object,
it's not part of the documented API surface
The message was ordered the wrong way in the TS rewrite.
Old:
+bookworm sent a CTCP request: "chadler" to version
New:
+bookworm sent a CTCP request: "version" to chadler