From 77229d9fc5362fd7649e4779b0bcbe61eb037840 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:40:30 +0000 Subject: [PATCH 1/5] Initial plan From 23a78f7fbeb0a74d570f2a807e7740bc2a479daa Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:44:52 +0000 Subject: [PATCH 2/5] Add optional compact parameter to party embeds - Removed global EMBED_FIELD_INLINE constant - Added 'compact' boolean field to party data structure (default: False) - Updated create_party_embed to use party['compact'] for inline field setting - Added optional compact parameter to party_create command - Updated CreatePartyModal, party_template_use, and party_list to use compact setting - Default behavior is NOT compact (inline=False) for better readability Co-authored-by: psykzz <1134201+psykzz@users.noreply.github.com> --- party/party.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/party/party.py b/party/party.py index d609846..3f6cdde 100644 --- a/party/party.py +++ b/party/party.py @@ -13,9 +13,6 @@ # Discord embed field character limit EMBED_FIELD_MAX_LENGTH = 1024 -# Whether embed fields should be displayed inline -EMBED_FIELD_INLINE = False - class RoleSelectionModal(discord.ui.Modal): """Modal for selecting a role when signing up for a party (for freeform entry).""" @@ -264,6 +261,7 @@ async def on_submit(self, interaction: discord.Interaction): "channel_id": None, "message_id": None, "scheduled_time": scheduled_time, + "compact": False, # Default to not compact (inline=False) } # Initialize signups for each predefined role @@ -1180,6 +1178,9 @@ async def create_party_embed(self, party: dict, guild: discord.Guild = None) -> color=discord.Color.blue() ) + # Get the compact setting (inline fields) - default to False (not compact) + compact = party.get("compact", False) + # Show scheduled time if set scheduled_time = party.get("scheduled_time") if scheduled_time: @@ -1188,7 +1189,7 @@ async def create_party_embed(self, party: dict, guild: discord.Guild = None) -> embed.add_field( name="📅 Scheduled Time", value=f"\n()", - inline=EMBED_FIELD_INLINE + inline=compact ) except (ValueError, OSError): pass @@ -1209,7 +1210,7 @@ async def create_party_embed(self, party: dict, guild: discord.Guild = None) -> else: value = "-" - embed.add_field(name=role, value=value, inline=EMBED_FIELD_INLINE) + embed.add_field(name=role, value=value, inline=compact) # Add roles that have signups but aren't in the predefined list (freeform roles) for role, users in signups.items(): @@ -1219,11 +1220,11 @@ async def create_party_embed(self, party: dict, guild: discord.Guild = None) -> value = ', '.join(user_mentions) if len(value) > EMBED_FIELD_MAX_LENGTH: value = value[:EMBED_FIELD_MAX_LENGTH-3] + "..." - embed.add_field(name=role, value=value, inline=EMBED_FIELD_INLINE) + embed.add_field(name=role, value=value, inline=compact) # If no roles defined and no signups, show a message if not roles and not any(users for users in signups.values()): - embed.add_field(name="Signups", value="-", inline=EMBED_FIELD_INLINE) + embed.add_field(name="Signups", value="-", inline=compact) # Get owner name for footer owner_name = await self._get_user_display_name(party['author_id'], guild) @@ -1243,7 +1244,8 @@ async def party_create( self, ctx, name: Optional[str] = None, - roles: Optional[str] = None + roles: Optional[str] = None, + compact: Optional[bool] = False ): """Create a new party with predefined roles. @@ -1260,6 +1262,8 @@ async def party_create( The name of the party roles : Optional[str] Space or comma-separated list of roles (e.g., "Tank Healer DPS" or "Tank, Healer, DPS") + compact : Optional[bool] + Display party in compact mode (inline fields). Default is False (not compact). Examples: - [p]party create (opens interactive modal) @@ -1268,6 +1272,7 @@ async def party_create( - [p]party create "Game Night" "Player1 Player2 Player3 Player4" - [p]party create "PvP Team" "Warrior, Mage, Archer" - [p]party create "Siege" "Siege Crossbow, Energy Shaper, GA" + - [p]party create "Compact Party" "Tank Healer DPS" True """ # If no arguments provided, show the modal if name is None: @@ -1352,6 +1357,7 @@ async def modal_button_callback(interaction: discord.Interaction): "channel_id": None, "message_id": None, "scheduled_time": None, + "compact": compact, # Use the compact parameter from command } # Initialize signups for each predefined role @@ -1522,7 +1528,9 @@ async def party_list(self, ctx): f"{time_text}" f"{link_text}" ) - embed.add_field(name=party["name"], value=value, inline=EMBED_FIELD_INLINE) + # Use party's compact setting, default to False + compact = party.get("compact", False) + embed.add_field(name=party["name"], value=value, inline=compact) await ctx.send(embed=embed) @@ -1887,7 +1895,8 @@ async def party_template_list(self, ctx): roles_text = ', '.join(template['roles']) if len(roles_text) > EMBED_FIELD_MAX_LENGTH: roles_text = roles_text[:EMBED_FIELD_MAX_LENGTH - 3] + "..." - embed.add_field(name=label, value=roles_text, inline=EMBED_FIELD_INLINE) + # Templates list display is not compact by default + embed.add_field(name=label, value=roles_text, inline=False) await ctx.send(embed=embed) @@ -1945,6 +1954,7 @@ async def party_template_use(self, ctx, template_name: str, *, title: str): "channel_id": None, "message_id": None, "scheduled_time": None, + "compact": False, # Default to not compact } # Initialize signups for each predefined role From 80ab4cffee393c31c4b14d9fc0782812967533ff Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sun, 19 Apr 2026 14:58:38 +0000 Subject: [PATCH 3/5] Add party compact command to edit existing parties - Added new `party compact` command to toggle compact mode for existing parties - Works for both newly created parties and pre-existing parties - Follows same permission pattern as other party edit commands (owner or admin) - Updates party message immediately and logs changes to modlog - Supports both True (compact/inline) and False (stacked) display modes Agent-Logs-Url: https://github.com/psykzz/cogs/sessions/0eca8b3f-be15-4623-a8d3-82b3d8ecc63e Co-authored-by: psykzz <1134201+psykzz@users.noreply.github.com> --- party/party.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/party/party.py b/party/party.py index 3f6cdde..593c667 100644 --- a/party/party.py +++ b/party/party.py @@ -1682,6 +1682,67 @@ def _fmt_ts(ts): f"✅ Scheduled time set for party `{party_id}`: ()" ) + @party.command(name="compact") + async def party_compact(self, ctx, party_id: str, compact: bool): + """Set the compact display mode for a party. + + Compact mode displays party fields inline (side-by-side). + Non-compact mode displays fields stacked vertically (default). + + Only the party creator or server admins can change this setting. + + Parameters + ---------- + party_id : str + The ID of the party to update + compact : bool + True for compact (inline) display, False for stacked display + + Examples: + - [p]party compact abc123 True + - [p]party compact abc123 False + """ + parties = await self.config.guild(ctx.guild).parties() + + if party_id not in parties: + await ctx.send("❌ Party not found.") + return + + party = parties[party_id] + + # Check permissions + is_author = party["author_id"] == ctx.author.id + is_admin = ctx.author.guild_permissions.administrator + + if not (is_author or is_admin): + await ctx.send("❌ You don't have permission to modify this party.") + return + + old_compact = party.get("compact", False) + + # Update compact setting + async with self.config.guild(ctx.guild).parties() as parties: + parties[party_id]["compact"] = compact + + # Update the message + await self.update_party_message(ctx.guild.id, party_id) + + # Create modlog entry + reason = ( + f"Party '{party['name']}' (ID: {party_id}) compact mode updated.\n" + f"Old: {'Compact' if old_compact else 'Not compact'}\n" + f"New: {'Compact' if compact else 'Not compact'}" + ) + await self.create_party_modlog( + ctx.guild, + "party_edit", + ctx.author, + reason + ) + + mode_text = "compact (inline)" if compact else "non-compact (stacked)" + await ctx.send(f"✅ Party `{party_id}` display mode set to **{mode_text}**.") + @party.command(name="rename-option") async def party_rename_option(self, ctx, party_id: str, old_option: str, *, new_option: str): """Rename an option/role in a party. From 148db5b861ba09d03510d7b82be85b4f747b7937 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:10:49 +0000 Subject: [PATCH 4/5] Add compact setting to edit and create party modals Agent-Logs-Url: https://github.com/psykzz/cogs/sessions/d1a2ceb9-f2f4-49e0-a61c-3b9a8db7d542 Co-authored-by: psykzz <1134201+psykzz@users.noreply.github.com> --- party/party.py | 136 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 114 insertions(+), 22 deletions(-) diff --git a/party/party.py b/party/party.py index edca8b5..f8f9bff 100644 --- a/party/party.py +++ b/party/party.py @@ -194,15 +194,16 @@ def __init__(self, cog): ) self.add_item(self.roles_input) - # Allow multiple signups per role - self.allow_multiple_input = discord.ui.TextInput( - label="Allow Multiple Per Role? (yes/no)", - placeholder="yes", + # Combined settings field (allow_multiple + compact) + self.settings_input = discord.ui.TextInput( + label="Settings (Optional)", + placeholder="allow_multiple=yes\ncompact=no", required=False, - max_length=3, - default="yes", + style=discord.TextStyle.paragraph, + max_length=100, + default="allow_multiple=yes\ncompact=no", ) - self.add_item(self.allow_multiple_input) + self.add_item(self.settings_input) # Scheduled date & time self.scheduled_time_input = discord.ui.TextInput( @@ -221,7 +222,7 @@ async def on_submit(self, interaction: discord.Interaction): title = self.title_input.value.strip() description = self.description_input.value.strip() or None roles_text = self.roles_input.value.strip() - allow_multiple_text = self.allow_multiple_input.value + settings_text = self.settings_input.value scheduled_time_text = self.scheduled_time_input.value.strip() # Validate title @@ -238,8 +239,8 @@ async def on_submit(self, interaction: discord.Interaction): ) return - # Parse and validate allow_multiple setting - allow_multiple, error = Party.parse_allow_multiple(allow_multiple_text) + # Parse and validate settings (allow_multiple + compact) + allow_multiple, compact, error = Party.parse_settings_text(settings_text) if error: await interaction.followup.send(error, ephemeral=True) return @@ -275,7 +276,7 @@ async def on_submit(self, interaction: discord.Interaction): "channel_id": None, "message_id": None, "scheduled_time": scheduled_time, - "compact": False, # Default to not compact (inline=False) + "compact": compact, # Use compact from settings field } # Initialize signups for each predefined role @@ -357,16 +358,19 @@ def __init__(self, party_id: str, party: dict, cog): ) self.add_item(self.roles_input) - # Allow multiple signups per role - allow_multiple_default = "yes" if party.get("allow_multiple_per_role", True) else "no" - self.allow_multiple_input = discord.ui.TextInput( - label="Allow Multiple Per Role? (yes/no)", - placeholder="yes or no", - default=allow_multiple_default, + # Combined settings field (allow_multiple + compact) + allow_multiple_val = "yes" if party.get("allow_multiple_per_role", True) else "no" + compact_val = "yes" if party.get("compact", False) else "no" + settings_default = f"allow_multiple={allow_multiple_val}\ncompact={compact_val}" + self.settings_input = discord.ui.TextInput( + label="Settings (Optional)", + placeholder="allow_multiple=yes\ncompact=no", + default=settings_default, required=False, - max_length=3, + style=discord.TextStyle.paragraph, + max_length=100, ) - self.add_item(self.allow_multiple_input) + self.add_item(self.settings_input) # Scheduled date & time scheduled_ts = party.get("scheduled_time") @@ -394,11 +398,21 @@ async def on_submit(self, interaction: discord.Interaction): new_title = self.title_input.value.strip() new_description = self.description_input.value.strip() or None roles_text = self.roles_input.value.strip() - allow_multiple_text = self.allow_multiple_input.value + settings_text = self.settings_input.value scheduled_time_text = self.scheduled_time_input.value.strip() - # Parse and validate allow_multiple setting - allow_multiple, error = Party.parse_allow_multiple(allow_multiple_text) + # Read current values as defaults so omitted keys leave the party unchanged + async with self.cog.config.guild(interaction.guild).parties() as _parties: + _current = _parties.get(self.party_id, {}) + _default_allow_multiple = _current.get("allow_multiple_per_role", True) + _default_compact = _current.get("compact", False) + + # Parse and validate settings (allow_multiple + compact) + allow_multiple, compact, error = Party.parse_settings_text( + settings_text, + default_allow_multiple=_default_allow_multiple, + default_compact=_default_compact, + ) if error: await interaction.followup.send(error, ephemeral=True) return @@ -428,12 +442,14 @@ async def on_submit(self, interaction: discord.Interaction): old_description = parties[self.party_id].get('description') old_roles = parties[self.party_id].get('roles', []) old_allow_multiple = parties[self.party_id].get('allow_multiple_per_role', True) + old_compact = parties[self.party_id].get('compact', False) old_scheduled_time = parties[self.party_id].get('scheduled_time') parties[self.party_id]['name'] = new_title parties[self.party_id]['description'] = new_description parties[self.party_id]['roles'] = unique_roles parties[self.party_id]['allow_multiple_per_role'] = allow_multiple + parties[self.party_id]['compact'] = compact parties[self.party_id]['scheduled_time'] = scheduled_time # Handle role changes: preserve signups for roles that still exist @@ -488,6 +504,8 @@ async def on_submit(self, interaction: discord.Interaction): changes.append(f"Removed roles affected {total_notified} user(s), DMs will be sent") if old_allow_multiple != allow_multiple: changes.append(f"Allow Multiple: {old_allow_multiple} → {allow_multiple}") + if old_compact != compact: + changes.append(f"Compact: {old_compact} → {compact}") if old_scheduled_time != scheduled_time: def _fmt_ts(ts): if ts is None: @@ -779,6 +797,80 @@ def parse_allow_multiple(allow_multiple_text: str) -> tuple[bool, Optional[str]] return allow_multiple, None + @staticmethod + def _parse_bool_value(value: str) -> Optional[bool]: + """Parse a yes/no/true/false string to bool, or None if empty.""" + v = value.strip().lower() + if v in ("yes", "true", "y", "1"): + return True + if v in ("no", "false", "n", "0"): + return False + return None # empty / unrecognised + + @staticmethod + def parse_settings_text( + settings_text: str, + default_allow_multiple: bool = True, + default_compact: bool = False, + ) -> tuple[bool, bool, Optional[str]]: + """Parse the combined settings field (allow_multiple + compact). + + Accepts one ``key=value`` or ``key: value`` pair per line. + Supported keys: ``allow_multiple``, ``compact``. + Unrecognised keys are ignored. Missing keys fall back to the + supplied defaults so that existing parties are not affected. + + Args: + settings_text: Raw text from the settings TextInput. + default_allow_multiple: Value to use when key is absent. + default_compact: Value to use when key is absent. + + Returns: + Tuple of (allow_multiple, compact, error_message). + error_message is None when the input is valid. + """ + allow_multiple = default_allow_multiple + compact = default_compact + + valid_keys = {"allow_multiple", "compact"} + valid_values = {"yes", "no", "true", "false", "y", "n", "1", "0", ""} + + for line in settings_text.splitlines(): + line = line.strip() + if not line: + continue + # Support both "key=value" and "key: value" + if "=" in line: + key, _, raw_val = line.partition("=") + elif ":" in line: + key, _, raw_val = line.partition(":") + else: + return allow_multiple, compact, ( + f"❌ Invalid settings format in '{line}'. " + "Use 'allow_multiple=yes' or 'compact=no'." + ) + + key = key.strip().lower() + raw_val = raw_val.strip().lower() + + if key not in valid_keys: + return allow_multiple, compact, ( + f"❌ Unknown setting '{key}'. " + "Supported settings: allow_multiple, compact." + ) + if raw_val not in valid_values: + return allow_multiple, compact, ( + f"❌ Invalid value '{raw_val}' for '{key}'. Use 'yes' or 'no'." + ) + + parsed = Party._parse_bool_value(raw_val) + if key == "allow_multiple": + allow_multiple = parsed if parsed is not None else default_allow_multiple + elif key == "compact": + compact = parsed if parsed is not None else default_compact + + return allow_multiple, compact, None + @staticmethod def parse_roles_from_text(roles_text: str) -> list[str]: """Parse roles from multiline text, removing duplicates while preserving order. From 20087bbd11c0109cba7b8c6cbbf1e57e389f9d35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:12:12 +0000 Subject: [PATCH 5/5] Fix spelling and remove unreachable empty-string from valid_values set Agent-Logs-Url: https://github.com/psykzz/cogs/sessions/d1a2ceb9-f2f4-49e0-a61c-3b9a8db7d542 Co-authored-by: psykzz <1134201+psykzz@users.noreply.github.com> --- party/party.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/party/party.py b/party/party.py index f8f9bff..86d5e2f 100644 --- a/party/party.py +++ b/party/party.py @@ -805,7 +805,7 @@ def _parse_bool_value(value: str) -> Optional[bool]: return True if v in ("no", "false", "n", "0"): return False - return None # empty / unrecognised + return None # empty / unrecognized @staticmethod def parse_settings_text( @@ -817,7 +817,7 @@ def parse_settings_text( Accepts one ``key=value`` or ``key: value`` pair per line. Supported keys: ``allow_multiple``, ``compact``. - Unrecognised keys are ignored. Missing keys fall back to the + Unrecognized keys are ignored. Missing keys fall back to the supplied defaults so that existing parties are not affected. Args: @@ -833,7 +833,7 @@ def parse_settings_text( compact = default_compact valid_keys = {"allow_multiple", "compact"} - valid_values = {"yes", "no", "true", "false", "y", "n", "1", "0", ""} + valid_values = {"yes", "no", "true", "false", "y", "n", "1", "0"} for line in settings_text.splitlines(): line = line.strip()