mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Fix bot kwargs and scoot over unread button to middle
This commit is contained in:
@@ -85,25 +85,38 @@ def _analyze_bot_signature(bot_func_or_sig) -> BotCallPlan:
|
||||
)
|
||||
|
||||
positional_capacity = len(positional_params)
|
||||
if not has_varargs and positional_capacity < 8:
|
||||
raise ValueError(
|
||||
"Bot function must accept at least 8 positional parameters before optional extras"
|
||||
)
|
||||
|
||||
base_args = [object()] * 8
|
||||
base_keyword_args: dict[str, object] = {
|
||||
"sender_name": object(),
|
||||
"sender_key": object(),
|
||||
"message_text": object(),
|
||||
"is_dm": object(),
|
||||
"channel_key": object(),
|
||||
"channel_name": object(),
|
||||
"sender_timestamp": object(),
|
||||
"path": object(),
|
||||
}
|
||||
candidate_specs: list[tuple[str, list[object], dict[str, object]]] = []
|
||||
if has_kwargs or explicit_optional_names:
|
||||
keyword_args = dict(base_keyword_args)
|
||||
if has_kwargs or "is_outgoing" in params:
|
||||
keyword_args["is_outgoing"] = False
|
||||
if has_kwargs or "path_bytes_per_hop" in params:
|
||||
keyword_args["path_bytes_per_hop"] = 1
|
||||
candidate_specs.append(("keyword", [], keyword_args))
|
||||
|
||||
if not has_kwargs and explicit_optional_names:
|
||||
kwargs: dict[str, object] = {}
|
||||
if has_kwargs or "is_outgoing" in params:
|
||||
kwargs["is_outgoing"] = False
|
||||
if has_kwargs or "path_bytes_per_hop" in params:
|
||||
kwargs["path_bytes_per_hop"] = 1
|
||||
candidate_specs.append(("keyword", base_args, kwargs))
|
||||
else:
|
||||
if has_varargs or positional_capacity >= 10:
|
||||
candidate_specs.append(("positional_10", base_args + [False, 1], {}))
|
||||
if has_varargs or positional_capacity >= 9:
|
||||
candidate_specs.append(("positional_9", base_args + [False], {}))
|
||||
candidate_specs.append(("mixed_keyword", base_args, kwargs))
|
||||
|
||||
if has_varargs or positional_capacity >= 10:
|
||||
candidate_specs.append(("positional_10", base_args + [False, 1], {}))
|
||||
if has_varargs or positional_capacity >= 9:
|
||||
candidate_specs.append(("positional_9", base_args + [False], {}))
|
||||
if has_varargs or positional_capacity >= 8:
|
||||
candidate_specs.append(("legacy", base_args, {}))
|
||||
|
||||
for call_style, args, kwargs in candidate_specs:
|
||||
@@ -111,7 +124,7 @@ def _analyze_bot_signature(bot_func_or_sig) -> BotCallPlan:
|
||||
sig.bind(*args, **kwargs)
|
||||
except TypeError:
|
||||
continue
|
||||
if call_style == "keyword":
|
||||
if call_style in {"keyword", "mixed_keyword"}:
|
||||
return BotCallPlan(call_style="keyword", keyword_args=tuple(kwargs.keys()))
|
||||
return BotCallPlan(call_style=call_style)
|
||||
|
||||
@@ -141,6 +154,7 @@ def execute_bot_code(
|
||||
|
||||
The code should define a function:
|
||||
`bot(sender_name, sender_key, message_text, is_dm, channel_key, channel_name, sender_timestamp, path, is_outgoing, path_bytes_per_hop)`
|
||||
or use named parameters / `**kwargs`.
|
||||
that returns either None (no response), a string (single response message),
|
||||
or a list of strings (multiple messages sent in order).
|
||||
|
||||
@@ -221,21 +235,27 @@ def execute_bot_code(
|
||||
)
|
||||
elif call_plan.call_style == "keyword":
|
||||
keyword_args: dict[str, Any] = {}
|
||||
if "sender_name" in call_plan.keyword_args:
|
||||
keyword_args["sender_name"] = sender_name
|
||||
if "sender_key" in call_plan.keyword_args:
|
||||
keyword_args["sender_key"] = sender_key
|
||||
if "message_text" in call_plan.keyword_args:
|
||||
keyword_args["message_text"] = message_text
|
||||
if "is_dm" in call_plan.keyword_args:
|
||||
keyword_args["is_dm"] = is_dm
|
||||
if "channel_key" in call_plan.keyword_args:
|
||||
keyword_args["channel_key"] = channel_key
|
||||
if "channel_name" in call_plan.keyword_args:
|
||||
keyword_args["channel_name"] = channel_name
|
||||
if "sender_timestamp" in call_plan.keyword_args:
|
||||
keyword_args["sender_timestamp"] = sender_timestamp
|
||||
if "path" in call_plan.keyword_args:
|
||||
keyword_args["path"] = path
|
||||
if "is_outgoing" in call_plan.keyword_args:
|
||||
keyword_args["is_outgoing"] = is_outgoing
|
||||
if "path_bytes_per_hop" in call_plan.keyword_args:
|
||||
keyword_args["path_bytes_per_hop"] = path_bytes_per_hop
|
||||
result = bot_func(
|
||||
sender_name,
|
||||
sender_key,
|
||||
message_text,
|
||||
is_dm,
|
||||
channel_key,
|
||||
channel_name,
|
||||
sender_timestamp,
|
||||
path,
|
||||
**keyword_args,
|
||||
)
|
||||
result = bot_func(**keyword_args)
|
||||
else:
|
||||
result = bot_func(
|
||||
sender_name,
|
||||
|
||||
@@ -835,45 +835,43 @@ export function MessageList({
|
||||
</div>
|
||||
|
||||
{/* Scroll to bottom button */}
|
||||
{(showJumpToUnread || showScrollToBottom) && (
|
||||
<div className="absolute bottom-4 right-4 flex items-center gap-2">
|
||||
{showJumpToUnread && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
unreadMarkerRef.current?.scrollIntoView?.({ block: 'center' });
|
||||
setShowJumpToUnread(false);
|
||||
}}
|
||||
className="h-9 rounded-full bg-card hover:bg-accent border border-border px-3 text-sm font-medium shadow-lg transition-all hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
Jump to unread
|
||||
</button>
|
||||
)}
|
||||
{showScrollToBottom && (
|
||||
<button
|
||||
onClick={scrollToBottom}
|
||||
className="w-9 h-9 rounded-full bg-card hover:bg-accent border border-border flex items-center justify-center shadow-lg transition-all hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
title="Scroll to bottom"
|
||||
aria-label="Scroll to bottom"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{showJumpToUnread && (
|
||||
<div className="pointer-events-none absolute bottom-4 left-1/2 -translate-x-1/2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
unreadMarkerRef.current?.scrollIntoView?.({ block: 'center' });
|
||||
setShowJumpToUnread(false);
|
||||
}}
|
||||
className="pointer-events-auto h-9 rounded-full bg-card hover:bg-accent border border-border px-3 text-sm font-medium shadow-lg transition-all hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
Jump to unread
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{showScrollToBottom && (
|
||||
<button
|
||||
onClick={scrollToBottom}
|
||||
className="absolute bottom-4 right-4 w-9 h-9 rounded-full bg-card hover:bg-accent border border-border flex items-center justify-center shadow-lg transition-all hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
title="Scroll to bottom"
|
||||
aria-label="Scroll to bottom"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Path modal */}
|
||||
{selectedPath && (
|
||||
|
||||
@@ -373,6 +373,30 @@ def bot(sender_name, sender_key, message_text, is_dm, channel_key, channel_name,
|
||||
)
|
||||
assert result == "bytes=2"
|
||||
|
||||
def test_pure_kwargs_bot_receives_core_fields_and_optional_extras(self):
|
||||
"""Pure **kwargs bots are first-class and receive the full payload by keyword."""
|
||||
code = """
|
||||
def bot(**kwargs):
|
||||
return (
|
||||
f"{kwargs.get('sender_name')}|{kwargs.get('message_text')}|"
|
||||
f"{kwargs.get('is_outgoing')}|{kwargs.get('path_bytes_per_hop')}"
|
||||
)
|
||||
"""
|
||||
result = execute_bot_code(
|
||||
code=code,
|
||||
sender_name="Alice",
|
||||
sender_key="abc123",
|
||||
message_text="Hi",
|
||||
is_dm=True,
|
||||
channel_key=None,
|
||||
channel_name=None,
|
||||
sender_timestamp=None,
|
||||
path="aabb",
|
||||
is_outgoing=True,
|
||||
path_bytes_per_hop=2,
|
||||
)
|
||||
assert result == "Alice|Hi|True|2"
|
||||
|
||||
def test_channel_message_with_none_sender_key(self):
|
||||
"""Channel messages correctly pass None for sender_key."""
|
||||
code = """
|
||||
@@ -489,6 +513,12 @@ class TestBotCodeValidation:
|
||||
}
|
||||
)
|
||||
|
||||
def test_pure_kwargs_code_passes(self):
|
||||
"""Pure **kwargs bots are valid."""
|
||||
from app.routers.fanout import _validate_bot_config
|
||||
|
||||
_validate_bot_config({"code": "def bot(**kwargs):\n return kwargs.get('message_text')"})
|
||||
|
||||
def test_syntax_error_raises(self):
|
||||
"""Syntax error in code raises HTTPException."""
|
||||
from fastapi import HTTPException
|
||||
|
||||
Reference in New Issue
Block a user