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:
1 parent
9341c4c1d9
commit
217625dcf7
2 files changed
+68
-7
No files matched your search
+13
-7
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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")]
|
||||
Reference in new issue
Block a user