Whispers used to flash on the recipient's terminal and disappear with
the next redraw. No history, no record, no signal if you weren't
looking.
Now whispers are stored per-recipient in a bounded inbox (16 slots,
FIFO eviction):
typedef struct { time_t timestamp; char from[]; char content[]; } whisper_t;
whisper_t whisper_inbox[16];
int whisper_inbox_count;
_Atomic int unread_whispers;
Sender side (:msg / :w):
- resolves target as before
- pushes the whisper into target->whisper_inbox under target->io_lock
(so two simultaneous senders to the same recipient don't tear the
ring)
- bumps target->unread_whispers atomically
- sends a single \a bell + triggers redraw_pending
- no longer writes whisper text directly to the channel (which used
to get clobbered by the next redraw)
Recipient side:
- new :inbox command in COMMAND mode prints the snapshot under
io_lock, in M7 chat-list style:
悄悄话 · whispers · 3
05-17 13:42 alice: 一会儿要不要喝咖啡
...
- viewing :inbox resets unread_whispers to 0
Title bar (extends UX-11):
- bright magenta "✉ N" chip alongside the yellow "★ N" mention chip
- same priority / degradation rules as ★
Whispers remain private — they're never broadcast to the room and
never persisted to messages.log. The inbox lives only in client_t,
so disconnecting drops it.
The bell + brief yellow highlight on the chat line meant that if you
weren't looking at the screen the moment someone @-mentioned you, you
had no way to know.
Now the title bar carries a sticky chip:
tester · 在线 3 · NORMAL ★ 2 ? 帮助
- bright yellow "★ N" appears whenever client->unread_mentions > 0
- count is bumped atomically in notify_mentions() for each target
- cleared automatically when the user returns to attention:
* pressing 'i' in NORMAL to re-enter INSERT mode
* pressing 'G' in NORMAL to jump to the live tail
- never dropped by the narrow-terminal degradation (UX-6) unless
every other optional chip has already been shed — it's the highest
priority signal in the bar
Counter is _Atomic int so the cross-thread bump in notify_mentions
doesn't tear against the local thread's reads / resets.
The original UX-10 was "give the client a readable reason on
disconnect" — turns out the libssh server API doesn't let us send
SSH_MSG_USERAUTH_BANNER, ssh_set_banner is GET-only on the linked
versions (0.9.6 on oss, 0.10.6 on ali), and pre-auth rejections
(max_connections / ratelimit / firewall) happen before any SSH
exchange the client could parse.
The realistic improvement is documentation: README now has a
troubleshooting table mapping the generic close to the actual cause
and how to verify (journalctl) + fix. Also documents the idle
timeout disconnect for completeness.
Server-side stderr already prints rejection reason with the
offending IP, so journalctl gives the admin enough to debug.
Typing @al<Tab> in INSERT mode now resolves to @alice and appends a
trailing space so the next word starts cleanly.
Algorithm:
1. walk back from end-of-input until '@' or ' ' is seen
2. '@' counts as a mention start only when at start-of-input or
preceded by a space (avoids matching e.g. email@host)
3. case-insensitive strncasecmp against current g_room usernames
4. first hit wins; the search ignores the local user when the prefix
is empty (so a lone "@<Tab>" defaults to the first *other* member,
matching the typical "ping someone" intent)
If the buffer is too short to hold "@<match> ", the completion is a
no-op rather than silently truncating the match.
Standard chat-client behaviour — much less typing for @mentions.
:list used to look like an ASCII printout from the 90s:
========================================
Online Users / 在线用户
========================================
Total / 总数: 3
----------------------------------------
* 1. alice (5m)
2. bob (12s)
3. carol (1h2m)
========================================
* = you / 你
Now it matches the rest of the TUI:
在线用户 · online · 3
▎ alice · 5m
bob · 12s
carol · 1h2m
- bold cyan title chip, dim grey total count
- 1-column ▎ gutter on your own row (same vocabulary as UX-1
message gutter)
- dim grey "· duration" separator instead of parentheses + ASCII rule
- no trailing rule, no legend explaining "* = you" because the
gutter speaks for itself