スキル一覧に戻る
lbds137

tzurot-slash-command-ux

by lbds137

A Discord bot that uses webhooks to represent multiple AI characters.

7🍴 1📅 2026年1月24日
GitHubで見るManusで実行

SKILL.md


name: tzurot-slash-command-ux description: 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. lastUpdated: '2026-01-23'

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

SubcommandPurposeExampleNotes
browsePaginated list/preset browsePreferred - has select menu
listSimple list/memory listLegacy - use browse for new cmds
viewSingle item detail/character view
createCreate new item/preset create
editModify item/character editOpens dashboard
deleteRemove item/me profile deleteMust confirm
setSet a value/me settings timezone
clearRemove/reset value/me overrides clear

Key Files

FilePurpose
src/commands/preset/browse.tsBrowse → Dashboard reference
src/utils/autocomplete/personalityAutocomplete.tsShared personality autocomplete
packages/common-types/src/utils/autocompleteFormat.tsAutocomplete formatting utility
src/utils/listSorting.tsShared sorting comparators
src/utils/customIds.tsCustom ID parsing/generation
src/utils/dashboard/settings/types.tsSettings custom ID builders/parsers
docs/reference/standards/SLASH_COMMAND_UX.mdFull UX documentation
docs/reference/standards/INTERACTION_PATTERNS.mdState 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

StatusEmojiMeaning
CompleteAll required filled
Partial⚠️Some optional empty
EmptyRequired 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

BadgeConstantUse For
🌐AUTOCOMPLETE_BADGES.GLOBALSystem-provided resource
🔒AUTOCOMPLETE_BADGES.OWNEDUser-created private
🌍AUTOCOMPLETE_BADGES.PUBLICUser-created shared
AUTOCOMPLETE_BADGES.DEFAULTCurrently active selection
🆓AUTOCOMPLETE_BADGES.FREEFree 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 codeUse shared paginationBuilder.ts
Delete without confirmationAlways confirm destructive actions
Show sensitive data publiclyUse ephemeral for user-specific data
Inconsistent naming (add vs create)Use standard subcommand names
  • 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

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です