瀏覽代碼

feat(telegram,discord): compact permission messages with expandable details (#952)

* feat(telegram,discord): compact permission messages with expandable details

Replace verbose permission request messages with a compact format showing
only the tool name. Adds a "See more" button that expands inline to show
tool_name, description, and pretty-printed input_preview JSON. Yes/No
buttons replace Allow/Deny. Bump plugin versions to 0.0.4.

* revert: restore Allow/Deny button labels
Noah Zweben 5 月之前
父節點
當前提交
4b1e2a28ce

+ 1 - 1
external_plugins/discord/.claude-plugin/plugin.json

@@ -1,7 +1,7 @@
 {
   "name": "discord",
   "description": "Discord channel for Claude Code \u2014 messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /discord:access.",
-  "version": "0.0.3",
+  "version": "0.0.4",
   "keywords": [
     "discord",
     "messaging",

+ 47 - 6
external_plugins/discord/server.ts

@@ -463,6 +463,9 @@ const mcp = new Server(
   },
 )
 
+// Stores full permission details for "See more" expansion keyed by request_id.
+const pendingPermissions = new Map<string, { tool_name: string; description: string; input_preview: string }>()
+
 // Receive permission_request from CC → format → send to all allowlisted DMs.
 // Groups are intentionally excluded — the security thread resolution was
 // "single-user mode for official plugins." Anyone in access.allowFrom
@@ -479,12 +482,14 @@ mcp.setNotificationHandler(
   }),
   async ({ params }) => {
     const { request_id, tool_name, description, input_preview } = params
+    pendingPermissions.set(request_id, { tool_name, description, input_preview })
     const access = loadAccess()
-    const text =
-      `🔐 Permission request [${request_id}]\n` +
-      `${tool_name}: ${description}\n` +
-      `${input_preview}`
+    const text = `🔐 Permission: ${tool_name}`
     const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
+      new ButtonBuilder()
+        .setCustomId(`perm:more:${request_id}`)
+        .setLabel('See more')
+        .setStyle(ButtonStyle.Secondary),
       new ButtonBuilder()
         .setCustomId(`perm:allow:${request_id}`)
         .setLabel('Allow')
@@ -734,11 +739,11 @@ client.on('error', err => {
 })
 
 // Button-click handler for permission requests. customId is
-// `perm:allow:<id>` or `perm:deny:<id>` — set when the request was sent.
+// `perm:allow:<id>`, `perm:deny:<id>`, or `perm:more:<id>`.
 // Security mirrors the text-reply path: allowFrom must contain the sender.
 client.on('interactionCreate', async (interaction: Interaction) => {
   if (!interaction.isButton()) return
-  const m = /^perm:(allow|deny):([a-km-z]{5})$/.exec(interaction.customId)
+  const m = /^perm:(allow|deny|more):([a-km-z]{5})$/.exec(interaction.customId)
   if (!m) return
   const access = loadAccess()
   if (!access.allowFrom.includes(interaction.user.id)) {
@@ -746,10 +751,46 @@ client.on('interactionCreate', async (interaction: Interaction) => {
     return
   }
   const [, behavior, request_id] = m
+
+  if (behavior === 'more') {
+    const details = pendingPermissions.get(request_id)
+    if (!details) {
+      await interaction.reply({ content: 'Details no longer available.', ephemeral: true }).catch(() => {})
+      return
+    }
+    const { tool_name, description, input_preview } = details
+    let prettyInput: string
+    try {
+      prettyInput = JSON.stringify(JSON.parse(input_preview), null, 2)
+    } catch {
+      prettyInput = input_preview
+    }
+    const expanded =
+      `🔐 Permission: ${tool_name}\n\n` +
+      `tool_name: ${tool_name}\n` +
+      `description: ${description}\n` +
+      `input_preview:\n${prettyInput}`
+    const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
+      new ButtonBuilder()
+        .setCustomId(`perm:allow:${request_id}`)
+        .setLabel('Allow')
+        .setEmoji('✅')
+        .setStyle(ButtonStyle.Success),
+      new ButtonBuilder()
+        .setCustomId(`perm:deny:${request_id}`)
+        .setLabel('Deny')
+        .setEmoji('❌')
+        .setStyle(ButtonStyle.Danger),
+    )
+    await interaction.update({ content: expanded, components: [row] }).catch(() => {})
+    return
+  }
+
   void mcp.notification({
     method: 'notifications/claude/channel/permission',
     params: { request_id, behavior },
   })
+  pendingPermissions.delete(request_id)
   const label = behavior === 'allow' ? '✅ Allowed' : '❌ Denied'
   // Replace buttons with the outcome so the same request can't be answered
   // twice and the chat history shows what was chosen.

+ 1 - 1
external_plugins/telegram/.claude-plugin/plugin.json

@@ -1,7 +1,7 @@
 {
   "name": "telegram",
   "description": "Telegram channel for Claude Code \u2014 messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /telegram:access.",
-  "version": "0.0.3",
+  "version": "0.0.4",
   "keywords": [
     "telegram",
     "messaging",

+ 36 - 6
external_plugins/telegram/server.ts

@@ -379,6 +379,9 @@ const mcp = new Server(
   },
 )
 
+// Stores full permission details for "See more" expansion keyed by request_id.
+const pendingPermissions = new Map<string, { tool_name: string; description: string; input_preview: string }>()
+
 // Receive permission_request from CC → format → send to all allowlisted DMs.
 // Groups are intentionally excluded — the security thread resolution was
 // "single-user mode for official plugins." Anyone in access.allowFrom
@@ -395,12 +398,11 @@ mcp.setNotificationHandler(
   }),
   async ({ params }) => {
     const { request_id, tool_name, description, input_preview } = params
+    pendingPermissions.set(request_id, { tool_name, description, input_preview })
     const access = loadAccess()
-    const text =
-      `🔐 Permission request [${request_id}]\n` +
-      `${tool_name}: ${description}\n` +
-      `${input_preview}`
+    const text = `🔐 Permission: ${tool_name}`
     const keyboard = new InlineKeyboard()
+      .text('See more', `perm:more:${request_id}`)
       .text('✅ Allow', `perm:allow:${request_id}`)
       .text('❌ Deny', `perm:deny:${request_id}`)
     for (const chat_id of access.allowFrom) {
@@ -686,11 +688,11 @@ bot.command('status', async ctx => {
 })
 
 // Inline-button handler for permission requests. Callback data is
-// `perm:allow:<id>` or `perm:deny:<id>` — set when the request was sent.
+// `perm:allow:<id>`, `perm:deny:<id>`, or `perm:more:<id>`.
 // Security mirrors the text-reply path: allowFrom must contain the sender.
 bot.on('callback_query:data', async ctx => {
   const data = ctx.callbackQuery.data
-  const m = /^perm:(allow|deny):([a-km-z]{5})$/.exec(data)
+  const m = /^perm:(allow|deny|more):([a-km-z]{5})$/.exec(data)
   if (!m) {
     await ctx.answerCallbackQuery().catch(() => {})
     return
@@ -702,10 +704,38 @@ bot.on('callback_query:data', async ctx => {
     return
   }
   const [, behavior, request_id] = m
+
+  if (behavior === 'more') {
+    const details = pendingPermissions.get(request_id)
+    if (!details) {
+      await ctx.answerCallbackQuery({ text: 'Details no longer available.' }).catch(() => {})
+      return
+    }
+    const { tool_name, description, input_preview } = details
+    let prettyInput: string
+    try {
+      prettyInput = JSON.stringify(JSON.parse(input_preview), null, 2)
+    } catch {
+      prettyInput = input_preview
+    }
+    const expanded =
+      `🔐 Permission: ${tool_name}\n\n` +
+      `tool_name: ${tool_name}\n` +
+      `description: ${description}\n` +
+      `input_preview:\n${prettyInput}`
+    const keyboard = new InlineKeyboard()
+      .text('✅ Allow', `perm:allow:${request_id}`)
+      .text('❌ Deny', `perm:deny:${request_id}`)
+    await ctx.editMessageText(expanded, { reply_markup: keyboard }).catch(() => {})
+    await ctx.answerCallbackQuery().catch(() => {})
+    return
+  }
+
   void mcp.notification({
     method: 'notifications/claude/channel/permission',
     params: { request_id, behavior },
   })
+  pendingPermissions.delete(request_id)
   const label = behavior === 'allow' ? '✅ Allowed' : '❌ Denied'
   await ctx.answerCallbackQuery({ text: label }).catch(() => {})
   // Replace buttons with the outcome so the same request can't be answered