The channel manager kept a receipt nobody could read

Every action banked a "created X" note that only surfaced once the
manager closed - by which time the refreshed list had already shown
the result. Worse, the notes accumulated: at three changes in one
visit the outcome outgrew the acknowledgement popup's line budget and
came back as the full-frame result window instead, so the same visit
reported itself two different ways depending on how much had been done
in it. The visit now acknowledges once, in the summary the tool
already returned. An error is the exception and is shown when it
happens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-09-08 23:50:13 -04:00
1 parent 9341c4c1d9
commit 217625dcf7
2 files changed
+68 -7

No files matched your search

+13 -7
View File
@@ -146,6 +146,16 @@ class ChannelSlot:
async def manage_channels(ctx: AppContext) -> int:
"""Run the interactive channel manager until the user backs out.
The visit acknowledges itself **once**, through the count returned here: the tool turns
it into a single line and the menu floats that as an OK popup. Each action used to bank
a "✓ created …" note of its own on top of that, which nobody could read until the
manager closed — by which time the refreshed list had already shown the result — and
three of them in one visit pushed the outcome past the popup's line budget (see
:func:`~meshterm.ui.surface._collapse_to_message`) and into the full result window, so
the same visit reported itself as a dialog or as a window depending on how much had been
done in it. An error is the exception, and is shown when it happens rather than banked
(see :func:`_import_link`).
Args:
ctx: Shared application context (provides the connected device and UI surface).
@@ -765,7 +775,6 @@ async def _create_private(
return 0
secret = random_secret()
await write_channel(ctx, device, idx, name.strip(), secret)
ctx.ui.note(f"[ok]✓[/ok] created private channel [brand]{name.strip()}[/brand]")
await _show_share(ctx, name.strip(), secret, intro="Share this channel:")
return 1
@@ -778,7 +787,6 @@ async def _add_default_public(
if idx is None:
return 0
await write_channel(ctx, device, idx, "Public", DEFAULT_PUBLIC_SECRET)
ctx.ui.note("[ok]✓[/ok] added the standard [brand]Public[/brand] channel")
return 1
@@ -801,7 +809,6 @@ async def _add_public(
name = f"#{name}"
secret = derive_secret(name) # what the firmware will compute; kept for the QR/share
await write_channel(ctx, device, idx, name, None) # None => firmware derives the key from name
ctx.ui.note(f"[ok]✓[/ok] added public channel [brand]{name}[/brand]")
await _show_share(ctx, name, secret, intro="Share this channel:")
return 1
@@ -833,7 +840,6 @@ async def _join_with_key(
name, key = answers
secret = normalize_secret(key)
await write_channel(ctx, device, idx, name.strip(), secret)
ctx.ui.note(f"[ok]✓[/ok] joined [brand]{name.strip()}[/brand]")
return 1
@@ -849,11 +855,13 @@ async def _import_link(
return 0
parsed = parse_share_url(url)
if parsed is None: # pragma: no cover - guarded by the validator
# Shown now, not banked: an error is about the action in front of the reader, and
# the only thing this one leaves behind is a slot that stayed empty.
ctx.ui.note("[err]not a valid channel link[/err]")
await ctx.ui.present(title="Channels")
return 0
name, secret = parsed
await write_channel(ctx, device, idx, name, secret)
ctx.ui.note(f"[ok]✓[/ok] imported [brand]{name}[/brand]")
return 1
@@ -890,7 +898,6 @@ async def _edit(ctx: AppContext, device: Device, slot: ChannelSlot) -> bool:
name = name.strip()
secret = normalize_secret(key) if key.strip() else None
await write_channel(ctx, device, slot.idx, name, secret)
ctx.ui.note(f"[ok]✓[/ok] updated channel [brand]{name}[/brand]")
return True
@@ -913,7 +920,6 @@ async def _clear(ctx: AppContext, device: Device, slot: ChannelSlot) -> bool:
ctx.chat.set_active(slot.conversation.key) # drop its unread before the slot goes away
ctx.chat.set_active(None)
await write_channel(ctx, device, slot.idx, "", None) # empty name => the slot reads as unused
ctx.ui.note(f"[warn]cleared channel {slot.name}[/warn]")
return True
+55
View File
@@ -35,6 +35,7 @@ from meshterm.persistence.repository import Repository
from meshterm.services.chat_service import ChatService
from meshterm.tools.channels import ChannelsTool
from meshterm.ui.channels import (
_CREATE,
ChannelSlot,
_apply_order,
_next_free_slot,
@@ -878,3 +879,57 @@ async def test_cli_clear_of_an_empty_slot_is_a_no_op(ctx: AppContext) -> None:
tool = ChannelsTool()
result = await tool.run(ctx, {"cli_action": "clear", "index": 7})
assert result.summary == {"index": 7, "cleared": False}
class _RecordingSession:
"""Records which surface :meth:`TuiUi.present` chose — the popup or the result window."""
def __init__(self) -> None:
self.shown: list[tuple[str, str]] = []
async def message_dialog(self, message, title: str = "") -> None: # noqa: ANN001
self.shown.append(("popup", title))
async def scroll(self, renderable, *, title: str = "", footer_hint: str = "") -> None: # noqa: ANN001
self.shown.append(("window", title))
class _PresentingUi(_ScriptedUi):
"""Scripted answers, but real note buffering — so what the visit *shows* can be asserted."""
def __init__(self, selects: list, texts: list, dialogs: list) -> None:
from meshterm.ui.surface import TuiUi
super().__init__(selects, texts, dialogs)
self.session_spy = _RecordingSession()
self.surface = TuiUi(self.session_spy) # no ``.session`` attribute: stays headless
def note(self, markup: str) -> None:
self.surface.note(markup)
def ack(self, markup: str) -> None:
self.surface.ack(markup)
async def test_a_busy_visit_still_reports_itself_in_a_popup(ctx: AppContext) -> None:
"""Three changes in one visit must acknowledge as a dialog, not the full result window.
Reproduces the reported bug: every action banked its own "✓ created …" note, so the
outcome grew a line per change and the third one pushed it past the popup's budget —
the same visit reported itself as a tidy dialog or as a full-frame window depending on
how much had been done in it. The visit now acknowledges once, in the tool's summary.
"""
ui = _PresentingUi(
selects=[_CREATE, _CREATE, _CREATE, None], # create three channels, then Esc
texts=["One", "Two", "Three"],
dialogs=[],
)
ctx.ui = ui
result = await ChannelsTool().run(ctx, {})
assert result.summary == {"changes": 3}
# The menu's own tail: note the tool's message, then present what the run buffered.
if result.message:
ui.surface.note(result.message)
await ui.surface.present(title="Channels")
assert ui.session_spy.shown == [("popup", "Channels")]