In a chat, the ends of our chip mark a message that names us

The star at the left of the time becomes two cells in the fill of our
chip: the notch, then the point, with no label between them. Thus the mark
has the colour of the mention chip in the body. Without chips, it is ">>"
in our hue. The Messages list puts the same mark at the left of the age.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5.5 committed 2026-10-13 22:42:28 -04:00
1 parent 427bee836b
commit 68d5d03324
9 files changed
+123 -48

No files matched your search

+4 -4
View File
@@ -15,10 +15,10 @@ can change how things behave. It does not only add new things.
your messages under `★` and your node's name, and the Messages list shows them in the
same way. Your name has the colour of your key, but in reverse: dark text on a light
fill, where each other sender has light text on a dark fill. A mention of your name has
the same reversed colours, and the message that has it gets a `★` at the left of its
time. Thus the messages that name you are easy to find down the left side of the chat.
On the PicoCalc, the console cannot draw a light fill, so there the `★` alone marks you.
Before, your messages showed only the `★`.
the same reversed colours, and the message that has it gets a small arrow mark in your
colour at the left of its time. Thus the messages that name you are easy to find down
the left side of the chat. On the PicoCalc, the console cannot draw a light fill, so
there the `★` marks your messages. Before, your messages showed only the `★`.
- **A mention has no `@` in front of it.** The coloured box around the name already says
that it is a mention. On a terminal that cannot draw the boxes, a mention keeps its `@`.
- **The line where you write a message shows each mention as a box.** When you type the
+6 -3
View File
@@ -488,9 +488,12 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow
the star in the chip's dark ink: a yellow star measured under 3:1 on the light fill for
238 of 256 hues, and the star on the page right of the chip was tried and put back (JP,
2026-10-13). A **mention** of us is the turned-over chip with no star
(`mention_chip(you=True)`); the message carrying it gets the `★` in the two cells left
of its time instead (`❯ ★ 12:34 body` — every time sits two cells right to keep the
column; the Messages list puts it left of the row's age). Arrow mode draws the chip as
(`mention_chip(you=True)`); the message carrying it gets **the two ends of our chip**
in the two cells left of its time instead — notch then point, no label, in our chip's
fill (`widgets.mention_mark` → `pathline.chip_ends`; `>>` in our hue in arrow mode;
it replaced a `★`, JP 2026-10-13). Every time sits two cells right to keep the column
(`❯`, a space, the mark, then `12:34 body`), and the Messages list puts the mark left
of the row's age. Arrow mode draws the chip as
a padded reverse-video block. The PicoCalc can't put a bright slot behind text, so
there the chip keeps the normal colours and the `★` alone marks us. With no name or key from the device, the bare `★` chip stays. Path lines
(delivery paths, the packet viewer, routes) keep `★`.
+9 -9
View File
@@ -57,7 +57,6 @@ from ..services.trace_runner import (
)
from ..ui import renderers
from ..ui.fields import ChannelRef, NodeRef
from ..ui.marks import SELF_MARK
from ..ui.menus import Lane, column_header, fit_cells, section_heading
from ..ui.pathline import PathHop
from ..ui.termfont import powerline_enabled
@@ -71,6 +70,7 @@ from ..ui.widgets import (
format_age,
hop_chip,
mention_chip,
mention_mark,
self_chip,
)
from .base import Tool, ToolResult, register
@@ -986,15 +986,15 @@ def _title(
text.append(" " * _BADGE_WIDTH)
# The relative age lane (right-aligned) is between the badge and the message text. Thus
# the ages are in one neat column, and all the previews start at the same position. A
# last message that names us has the ``★`` at the left of its age, the same as the
# transcript has it at the left of the time (refer to ChatScreen._body_lines).
# last message that names us has the ends of our chip at the left of its age, the same
# as the transcript has them at the left of the time (refer to ChatScreen._body_lines).
age = _ago(last.created_at) if last is not None else ""
lane = f" {age:>{_AGE_WIDTH}}"
if age and last is not None and _names_us(last, us):
star = len(lane) - len(age) - 2 # the star, a space, then the age
text.append(lane[:star])
text.append(SELF_MARK[0], style="type.you")
text.append(lane[star + 1 :], style="muted")
mark = len(lane) - len(age) - 2 # the two cells of the mark, then the age
text.append(lane[:mark])
text.append_text(mention_mark(us[1]))
text.append(age, style="muted")
else:
text.append(lane, style="muted")
text.append(" ")
@@ -1057,8 +1057,8 @@ def _preview_text(
- Our own messages start with our chip, ``★ Name`` turned over (refer to
:func:`~meshterm.ui.widgets.self_chip`). A mention of our name has the chip that is
turned over, with no star. The star of a message that names us is at the left of the
age of the row (refer to :func:`_title`).
turned over, with no star. A message that names us has the ends of our chip at the
left of the age of the row (refer to :func:`_title`).
- In a received channel message, the inline ``Name:`` sender is the author, in the hue
that comes from its key (the node grey when no known node has the name).
- A room post starts with its author, named by key in the same way as the room screen
+22 -14
View File
@@ -33,7 +33,6 @@ from ..core.models import ChatMessage, Contact, Conversation, Message, utcnow
from ..core.regions import WILDCARD
from ..platforms import Platform, on_platform
from .compose import MentionEditor
from .marks import SELF_MARK
from .pathline import PATH_INK, PathHop
from .qr import QrScreen, qr_strip
from .theme import snr_style
@@ -45,7 +44,15 @@ from .tui.prompt import (
from .tui.render import render_hanging, render_lines, right_aligned_tail
from .tui.screen import CANCEL, Screen
from .tui.spinner import Spinner, spinner_interval
from .widgets import DESTINATION, ORIGIN, hop_chip, mention_hop, name_chip, self_chip
from .widgets import (
DESTINATION,
ORIGIN,
hop_chip,
mention_hop,
mention_mark,
name_chip,
self_chip,
)
if TYPE_CHECKING:
from ..context import AppContext
@@ -69,7 +76,7 @@ _WORD = re.compile(r"\S+")
#: The column where a URL starts when it is too long to hang under its body: the column
#: after the ``❯`` that is in front of the first line of the selected message. A message
#: that names us has its ``★`` in this column, and the timestamp follows it.
#: that names us has its mark in this column, and the timestamp follows it.
_URL_GUTTER = 2
#: Whether the URLs of a message are drawn as QR codes under it (``Platform.url_codes``,
@@ -768,12 +775,14 @@ class ChatScreen(Screen):
is marked as the reply target (the same as the ``❯`` pointer and the highlight of
the select screen).
A message that mentions our name has the ``★`` of the app at the left of its time
(JP, 2026-10-13). The chip of the mention in the body is turned over, but it has no
star (refer to :func:`~meshterm.ui.widgets.mention_hop`). Thus the star of a message
that names us is in one column, where the eye finds it when it goes down the
transcript. Each time has the two cells of that column in front of it, so that all
the times stay in one column too.
A message that mentions our name has a mark at the left of its time: the two ends
of our chip, in its colour, with no label (refer to
:func:`~meshterm.ui.widgets.mention_mark`, JP, 2026-10-13). The chip of the mention
in the body is turned over, but it has no star (refer to
:func:`~meshterm.ui.widgets.mention_hop`). Thus the mark of a message that names us
is in one column, where the eye finds it when it goes down the transcript. Each time
has the two cells of that column in front of it, so that all the times stay in one
column too.
A URL is never cut where the screen can hold it. A URL that is too long for the
body lane moves out to the column after the ``❯``, on its own line (refer to
@@ -789,8 +798,7 @@ class ChatScreen(Screen):
prefix = Text()
prefix.append("❯ " if selected else " ", style="cursor" if selected else None)
if self._names_us(body):
prefix.append(SELF_MARK[0], style="type.you")
prefix.append(" ")
prefix.append_text(mention_mark(self._self_key))
else:
prefix.append(" ")
prefix.append(f"{stamp:%H:%M} ", style="cursor" if selected else "muted")
@@ -847,8 +855,8 @@ class ChatScreen(Screen):
Thus the user sees that a mention belongs to the node that it names. The text
around the mentions keeps the base style of the line (``cursor`` when the message
is the selected reply target, or else no style). A mention of our own name has our
chip, turned over, and the line has a ``★`` at the left of its time (refer to
:meth:`_body_lines`). Thus a message that names us is easy to see.
chip, turned over, and the line has the ends of our chip at the left of its time
(refer to :meth:`_body_lines`). Thus a message that names us is easy to see.
The chip opens on the notch, because the node that a mention names is the
destination of a route that came from the sender (refer to :meth:`_group_header`,
@@ -881,7 +889,7 @@ class ChatScreen(Screen):
return mention_hop(name, self._sender_key(name, mention=True))
def _names_us(self, body: str) -> bool:
"""Whether ``body`` has a mention of our name (it gets a ``★`` before its time)."""
"""Whether ``body`` has a mention of our name (it gets a mark before its time)."""
if not self._self_name:
return False
return any(match.group(1) == self._self_name for match in MENTION.finditer(body))
+21
View File
@@ -1294,6 +1294,27 @@ def _bind_chips(platform: Platform) -> None:
_colours_of, _WEIGHT, _FLAT = _slot_colours, "not bold ", "not bold "
def chip_ends(hop: PathHop) -> Text:
"""The two ends of the chip of ``hop`` with no label between them: the notch, then the point.
It is two cells in the fill of the chip, the same two cells that open and close a chip
of a destination (refer to :meth:`PathLine._notch`). A chat puts it at the left of the
time of a message that names us, in the fill of our chip (refer to
:func:`~meshterm.ui.widgets.mention_mark`). The colours come from the palette of the
platform, so the PicoCalc draws it in the fill that the chip has there.
Args:
hop: The hop whose chip gives the fill.
Returns:
The two styled cells.
"""
fill = _colours_of(hop).fill
text = PathLine._notch(fill)
text.append(POWERLINE_SEP, style=f"{_FLAT}{fill}")
return text
def _shorten(value: str, hash_bytes: int | None) -> str:
"""Bare hex in lower case, truncated to ``hash_bytes`` bytes (whole when it is empty)."""
raw = value.lower().removeprefix("0x")
-5
View File
@@ -173,10 +173,6 @@ MESH_THEME = Theme(
"type.repeater": "#a78bfa",
"type.room": "#ffffff",
"type.sensor": "#fb923c",
# The ``★`` of our node where it stands alone on the page as a mark: after our name
# in a chat, and before the time of a message that names us. It is the yellow of
# the map (``marks.SELF_MARK``), with a name so that the console chooses its slot.
"type.you": "#facc15",
# A region name (the scope of a flood). It is not a node, so it has no hue from the
# node wheel. It is a light slate. Its slant makes it different from prose, as a
# tag is different from a sentence.
@@ -416,7 +412,6 @@ MESH_THEME_16 = Theme(
"type.repeater": "not bold color(5)",
"type.room": "color(15)",
"type.sensor": "not bold color(3)",
"type.you": "color(11)",
# A region name: the VT has no italic, and each chromatic slot is a node hue. Thus
# the style is the plain light grey. The word "scope" in front of it makes it
# different from other text.
+26 -3
View File
@@ -57,7 +57,7 @@ from .marks import (
LabelRgbOf,
parse_hex,
)
from .pathline import SELF_GLYPH, PathHop, PathLine, path_line
from .pathline import SELF_GLYPH, PathHop, PathLine, chip_ends, path_line
from .termfont import powerline_enabled
from .theme import glyph, mark_rgb, name_style, node_style, snr_style
@@ -698,8 +698,8 @@ def self_chip(name: str | None, key: str | None, *, role: str | None = None) ->
:attr:`~meshterm.ui.pathline.PathHop.inverse`).
A mention of our name is a different mark (refer to :func:`mention_chip`). The chip is
turned over, but it has no star. The star of a message that names us is at the left of
the time of the message (refer to :meth:`~meshterm.ui.chat.ChatScreen._body_lines`).
turned over, but it has no star. The message that names us has the ends of our chip at
the left of its time (refer to :func:`mention_mark`).
Our node with no name or no key (the device did not report them) keeps the bare ``★``
chip of a route.
@@ -717,6 +717,29 @@ def self_chip(name: str | None, key: str | None, *, role: str | None = None) ->
return hop_chip(PathHop(f"{SELF_GLYPH} {name}", key=key, inverse=True), role=role)
def mention_mark(key: str | None) -> Text:
"""The mark of a message that names us: the two ends of our chip, with no label.
The notch, then the point, in the fill of our chip (refer to
:func:`~meshterm.ui.pathline.chip_ends`). The chat puts it in the two cells at the left
of the time of each message that mentions our name, and the Messages list puts it at
the left of the age of a row whose last message does (JP, 2026-10-13). It replaced a
``★``. The mark has the colour of the chip that the mention has in the body, so the eye
joins the two. Where the terminal draws no chips, it is ``>>`` in our hue, the colour
that our chip has there.
Args:
key: Our public key, which gives the hue. With ``None``, the mark has the keyless
grey, the same as the chip of the mention.
Returns:
The mark, two cells wide.
"""
if powerline_enabled():
return chip_ends(PathHop("", key=key or None, inverse=bool(key)))
return Text(">>", style=node_style(key) if key else "node.unknown")
def mention_hop(name: str, key: str | None, *, you: bool = False) -> PathHop:
"""The hop of a mention: the node that a message names, as its chip shows it.
+14 -10
View File
@@ -1608,7 +1608,7 @@ def test_a_blank_line_closes_each_author_where_the_rows_allow(
def test_a_mention_of_our_name_takes_our_chip_with_no_star(powerline) -> None:
"""The chip of a mention of us is turned over, as our sender chip, but it has no star.
The star of a message that names us is at the left of its time (refer to the next
The mark of a message that names us is at the left of its time (refer to the next
test). With chips, no mention has its ``@``.
"""
from meshterm.ui.theme import node_style
@@ -1621,8 +1621,12 @@ def test_a_mention_of_our_name_takes_our_chip_with_no_star(powerline) -> None:
assert str(me.style).endswith(f"on {node_style(_OUR_KEY).split()[-1]}") # a light fill
def test_a_message_that_names_us_has_the_star_left_of_its_time() -> None:
"""The ``★`` goes in the two cells before the time, and each time moves two cells right."""
def test_a_message_that_names_us_has_the_mark_left_of_its_time() -> None:
"""The mark goes in the two cells before the time, and each time moves two cells right.
The suite draws no chips, so the mark is ``>>`` in our hue (refer to the widget tests
for the two ends of the chip).
"""
from datetime import datetime, timezone
at = datetime(2026, 7, 5, 14, 24, tzinfo=timezone.utc)
@@ -1633,12 +1637,12 @@ def test_a_message_that_names_us_has_the_star_left_of_its_time() -> None:
]
screen = _channel_screen(messages, self_name="Me", self_key=_OUR_KEY)
rows = _strip_ansi("\n".join(screen._render_grouped(72))).splitlines()
assert any(row.startswith(f" ★ {stamp} hi") for row in rows)
assert any(row.startswith(f" >>{stamp} hi") for row in rows)
assert any(row.startswith(f" {stamp} hi") for row in rows) # the other message
screen.handle("up")
screen.handle("up") # the first message: the pointer, then the star
screen.handle("up") # the first message: the pointer, then the mark
rows = _strip_ansi("\n".join(screen._render_grouped(72))).splitlines()
assert any(row.startswith(f"❯ ★ {stamp} hi") for row in rows)
assert any(row.startswith(f"❯ >>{stamp} hi") for row in rows)
def _channel_screen(
@@ -1750,7 +1754,7 @@ def test_the_compose_line_keeps_a_chip_whole_when_it_wraps(powerline) -> None:
assert sum(chip in line for line in lines) == 1, width
def test_a_row_whose_last_message_names_us_has_the_star_left_of_its_age(
def test_a_row_whose_last_message_names_us_has_the_mark_left_of_its_age(
repo: Repository,
) -> None:
"""The Messages list marks the row as the transcript marks the message."""
@@ -1760,10 +1764,10 @@ def test_a_row_whose_last_message_names_us_has_the_star_left_of_its_age(
named = ChatMessage(text="Bob: hi @[Me]", is_channel=True, channel_id="c0")
other = ChatMessage(text="Bob: hi @[Zed]", is_channel=True, channel_id="c0")
lanes = _lanes([conv])
starred = _title(ctx, conv, {"chan:c0": named}, _NO_KEYS, lanes, us=us).plain
marked = _title(ctx, conv, {"chan:c0": named}, _NO_KEYS, lanes, us=us).plain
plain = _title(ctx, conv, {"chan:c0": other}, _NO_KEYS, lanes, us=us).plain
assert "★ now " in starred and "★" not in plain
assert starred.index("now") == plain.index("now") # the star takes a cell of the gap
assert ">>now " in marked and ">>" not in plain
assert marked.index("now") == plain.index("now") # the mark takes the cells of the gap
def test_channel_typing_clears_selection() -> None:
+21
View File
@@ -30,6 +30,7 @@ from meshterm.ui.widgets import (
hop_chip,
mention_chip,
mention_hop,
mention_mark,
name_chip,
path_text,
revisit_note,
@@ -398,6 +399,26 @@ def test_a_mention_of_us_is_turned_over_with_no_star(powerline) -> None:
assert not mention_hop("Me", None, you=True).inverse # no key: no hue to turn over
def test_the_mark_of_a_message_that_names_us_is_the_ends_of_our_chip(powerline) -> None:
"""The notch, then the point, in the fill of our chip, and no label between them."""
powerline(True)
mark = mention_mark("5e7f")
fill = _span_style(mention_chip("Me", "5e7f", you=True), "Me").bgcolor.name
assert mark.plain == POWERLINE_SEP * 2
notch, point = (Style.parse(str(span.style)) for span in mark.spans)
assert notch.reverse and notch.color.name == fill # cut out of the fill
assert not point.reverse and point.color.name == fill # the point of the fill
def test_the_mark_without_chips_is_two_arrows_in_our_hue(powerline) -> None:
"""With no chips, the mark is ``>>``, in the hue that our chip has in arrow mode."""
powerline(False)
mark = mention_mark("5e7f")
assert mark.plain == ">>" and str(mark.style) == node_style("5e7f")
assert str(mention_mark(None).style) == "node.unknown"
def test_self_chip_keeps_the_bare_star_without_our_name_or_key(powerline) -> None:
"""A device that did not report our name or key leaves the star of a route."""
powerline(True)