搜索

PythonReactSEOPDF
GitHub Skill

tzurot-slash-command-ux

Use when implementing Discord slash commands, building list/browse UX, adding buttons or pagination, or creating dashboard patterns. Covers standard naming, shared utilities, and confirmation flows.

lbds137☆ 7 仓库 Stars1 仓库 Forks

安装与使用

请根据来源仓库和你的 AI 工具说明安装此技能。先阅读技能文件和依赖要求,再添加到工作流。

查看来源 ↗使用指南

技能说明

Slash Command UX Patterns

Use this skill when:

  • Implementing a new slash command or subcommand
  • Adding list/browse functionality with pagination
  • Creating button-based interactions (confirm, navigate)
  • Building dashboard-style editors

Quick Reference

Standard Subcommand Names
Subcommand Purpose Example Notes
browse Paginated list /preset browse Preferred - has select menu
list Simple list /memory list Legacy - use browse for new cmds
view Single item detail /character view
create Create new item /preset create
edit Modify item /character edit Opens dashboard
delete Remove item /me profile delete Must confirm
set Set a value /me settings timezone
clear Remove/reset value /me overrides clear
Key Files
File Purpose
src/commands/preset/browse.ts Browse → Dashboard reference
src/utils/autocomplete/personalityAutocomplete.ts Shared personality autocomplete
packages/common-types/src/utils/autocompleteFormat.ts Autocomplete formatting utility
src/utils/listSorting.ts Shared sorting comparators
src/utils/customIds.ts Custom ID parsing/generation
src/utils/dashboard/settings/types.ts Settings custom ID builders/parsers
docs/reference/standards/SLASH_COMMAND_UX.md Full UX documentation
docs/reference/standards/INTERACTION_PATTERNS.md State passing patterns guide

Pagination Pattern

Button Layout
[◀ Previous] [Page 1 of 5] [Next ▶] [🔤 Sort A-Z]
Custom ID Format
// Custom IDs encode action, page, and sort type
const customId = `${prefix}:page:${pageNum}:${sortType}`;
// Example: "memory:page:2:date" or "character:sort:0:name"
Implementation Pattern
// 1. Define constants
const ITEMS_PER_PAGE = 15;
const DEFAULT_SORT: ListSortType = 'date';

// 2. Use shared comparator from listSorting.ts
import { createListComparator } from '../../utils/listSorting.js';
const comparator = createListComparator<ItemType>(
  item => item.name, // name accessor for A-Z sort
  item => item.createdAt // date accessor for chronological
);

// 3. Build pagination buttons
function buildButtons(page: number, totalPages: number, sort: ListSortType) {
  return new ActionRowBuilder<ButtonBuilder>().addComponents(
    new ButtonBuilder()
      .setCustomId(`prefix:page:${page - 1}:${sort}`)
      .setLabel('◀ Previous')
      .setStyle(ButtonStyle.Secondary)
      .setDisabled(page === 0)
    // ... page indicator, next button, sort toggle
  );
}

// 4. Handle button clicks
collector.on('collect', (interaction: ButtonInteraction) => {
  const parsed = parseCustomId(interaction.customId);
  // Re-fetch data, re-sort, update embed
  await interaction.update({ embeds: [newEmbed], components: [newButtons] });
});

Browse → Dashboard Pattern (NEW)

Standard flow for list commands. Select menu lets users pick an item to view/edit.

Layout
┌──────────────────────────────────────────────────────────────┐
│  📚 Preset Browser                                           │
│  1. 🌐⭐ Global Default · claude-sonnet-4                    │
│  2. 🔒 My Preset · gpt-4o                                    │
├──────────────────────────────────────────────────────────────┤
│  [▼ Select a preset to view...]                              │
│  [◀ Prev]  Page 1 of 3  [Next ▶]  [🔤 Sort A-Z]             │
└──────────────────────────────────────────────────────────────┘
Implementation
// 1. Build select menu for current page items
function buildBrowseSelectMenu(
  pageItems: Preset[],
  startIdx: number
): ActionRowBuilder<StringSelectMenuBuilder> {
  const selectMenu = new StringSelectMenuBuilder()
    .setCustomId('preset::browse-select')
    .setPlaceholder('Select a preset to view...');

  pageItems.forEach((preset, index) => {
    selectMenu.addOptions(
      new StringSelectMenuOptionBuilder()
        .setLabel(`${startIdx + index + 1}. ${preset.name}`)
        .setValue(preset.id)
        .setDescription(preset.model)
    );
  });

  return new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu);
}

// 2. Handle select menu interaction
export async function handleBrowseSelect(interaction: StringSelectMenuInteraction): Promise<void> {
  const presetId = interaction.values[0];
  await interaction.deferUpdate();

  // Fetch and open dashboard (same as /preset edit)
  const preset = await fetchPreset(presetId, interaction.user.id);
  const embed = buildDashboardEmbed(PRESET_DASHBOARD_CONFIG, preset);
  const components = buildDashboardComponents(PRESET_DASHBOARD_CONFIG, presetId, preset);

  await interaction.editReply({ embeds: [embed], components });
}
Reference Implementation
  • services/bot-client/src/commands/preset/browse.ts - Full pattern

Dashboard Pattern

Use for entities with 6+ editable fields that don't fit in a single modal.

Flow
1. /command create → Minimal seed modal (3-4 required fields)
2. Entity created → Dashboard embed with section menu
3. User selects section → Section modal (max 5 fields)
4. Submit → Dashboard refreshes
Status Indicators
Status Emoji Meaning
Complete ✅ All required filled
Partial ⚠️ Some optional empty
Empty ❌ Required missing

Example: /character edit uses this pattern for personality settings.

Confirmation Patterns

Destructive Actions
// Level 1: Simple button confirm (single item)
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
  new ButtonBuilder().setCustomId('cancel').setLabel('Cancel').setStyle(ButtonStyle.Secondary),
  new ButtonBuilder()
    .setCustomId(`delete:${itemId}`)
    .setLabel('Delete')
    .setStyle(ButtonStyle.Danger) // Red button for destructive
);
// Level 2: Typed confirmation (bulk operations)
const modal = new ModalBuilder()
  .setCustomId(`purge-confirm:${entityId}`)
  .setTitle('Confirm Deletion')
  .addComponents(
    new ActionRowBuilder<TextInputBuilder>().addComponents(
      new TextInputBuilder()
        .setCustomId('confirmation')
        .setLabel('Type "DELETE ALL" to confirm')
        .setStyle(TextInputStyle.Short)
        .setRequired(true)
    )
  );

Response Types

When to Use Ephemeral
// ✅ Use ephemeral for:
await interaction.reply({
  content: 'Settings updated',
  flags: MessageFlags.Ephemeral, // Private to user
});
// - User settings/preferences
// - Error messages
// - Dashboard interactions
// - Sensitive data (API keys, stats)

// ✅ Use public for:
// - Character/personality displays others might want to see
// - Help text (optional - can be ephemeral too)
Defer for Slow Operations
// If operation takes >3 seconds
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
// ... slow database/API call ...
await interaction.editReply({ content: 'Done!' });

Error Handling

// ❌ BAD - Vague error
await interaction.reply({ content: '❌ Error', flags: MessageFlags.Ephemeral });

// ✅ GOOD - Actionable error
await interaction.reply({
  content: '❌ Character not found.\n\nUse `/character list` to see available characters.',
  flags: MessageFlags.Ephemeral,
});

Autocomplete

Use for entity selection (characters, presets, personalities) and large lists (>10 items).

Standard Formatting (REQUIRED)

Use the shared formatAutocompleteOption utility for consistent badge formatting:

import { formatAutocompleteOption, AUTOCOMPLETE_BADGES } from '@tzurot/common-types';

const choices = items.map(item =>
  formatAutocompleteOption({
    name: item.name,
    value: item.id,
    scopeBadge: item.isGlobal ? AUTOCOMPLETE_BADGES.GLOBAL : AUTOCOMPLETE_BADGES.OWNED,
    statusBadges: item.isDefault ? [AUTOCOMPLETE_BADGES.DEFAULT] : undefined,
    metadata: item.model?.split('/').pop(), // Short model name
  })
);
// Result: "🌐⭐ Global Default · claude-sonnet-4"
Badge Reference
Badge Constant Use For
🌐 AUTOCOMPLETE_BADGES.GLOBAL System-provided resource
🔒 AUTOCOMPLETE_BADGES.OWNED User-created private
🌍 AUTOCOMPLETE_BADGES.PUBLIC User-created shared
⭐ AUTOCOMPLETE_BADGES.DEFAULT Currently active selection
🆓 AUTOCOMPLETE_BADGES.FREE Free tier model
Shared Utilities

For personality/character autocomplete, use the shared handlers:

import { handlePersonalityAutocomplete } from '../../utils/autocomplete/index.js';

// Handles filtering, caching, and badge formatting
await handlePersonalityAutocomplete(interaction, {
  optionName: 'personality',
  ownedOnly: false,
  showVisibility: true,
});

Anti-Patterns

❌ Don't ✅ Do
Expose internal concepts (/llm-config) Use user-friendly names (/preset)
Duplicate pagination code Use shared paginationBuilder.ts
Delete without confirmation Always confirm destructive actions
Show sensitive data publicly Use ephemeral for user-specific data
Inconsistent naming (add vs create) Use standard subcommand names

Related Skills

  • tzurot-architecture - Service boundaries for command handlers
  • tzurot-testing - Testing button interactions and collectors
  • tzurot-types - Shared response types and Zod schemas

References

  • Full UX documentation: docs/reference/standards/SLASH_COMMAND_UX.md
  • State passing patterns: docs/reference/standards/INTERACTION_PATTERNS.md
  • Features & implementation: docs/reference/features/SLASH_COMMAND_UX_FEATURES.md
  • Character list example: services/bot-client/src/commands/character/list.ts
  • Channel list example: services/bot-client/src/commands/channel/list.ts
  • Shared sorting: services/bot-client/src/utils/listSorting.ts

使用示例

查看来源仓库中的使用示例与配置说明。

查看来源 ↗

来源历史

收录时间
2026-01-20
索引更新时间
2026-01-26
Branch
main

收录与同步日期属于本站记录。源码修改历史请查看来源仓库。

GitHub ↗

chat Comments (0)

chat_bubble_outline

No comments yet. Be the first to share your thoughts!