1- import { Client , GatewayIntentBits , Partials } from "discord.js" ;
1+ import {
2+ ActionRowBuilder ,
3+ ButtonBuilder ,
4+ ButtonStyle ,
5+ Client ,
6+ GatewayIntentBits ,
7+ Partials ,
8+ } from "discord.js" ;
29import { createCoreRuntime } from "@/core/runtime" ;
310import type { IMAdapter } from "@/core/types" ;
411import { createAgentAdapter } from "@/agents/adapter" ;
@@ -176,8 +183,18 @@ function getLocalSettingsUrl(): string {
176183 return `http://${ getWebHost ( ) } :${ getWebPort ( ) } /local-setting` ;
177184}
178185
186+ type LauncherCommand = "setting" | "channel" | "gh" ;
187+
188+ function getResolvedChannelId ( target : any ) : string {
189+ const channel = target ?. channel ;
190+ if ( channel ?. isThread ?.( ) ) {
191+ return channel . parentId ?? target . channelId ;
192+ }
193+ return target . channelId ;
194+ }
195+
179196function buildLauncherCommandText ( params : {
180- command : "setting" | "channel" | "gh" ;
197+ command : LauncherCommand ;
181198 userId : string ;
182199 channelId : string ;
183200} ) : string {
@@ -198,6 +215,77 @@ function buildLauncherCommandText(params: {
198215 return `Open settings: ${ settingsUrl } ` ;
199216}
200217
218+ function buildSettingsLinkRow ( ) : ActionRowBuilder < ButtonBuilder > {
219+ return new ActionRowBuilder < ButtonBuilder > ( ) . addComponents (
220+ new ButtonBuilder ( )
221+ . setStyle ( ButtonStyle . Link )
222+ . setLabel ( "Open settings" )
223+ . setURL ( getLocalSettingsUrl ( ) )
224+ ) ;
225+ }
226+
227+ function buildSettingsChooserRows ( channelId : string ) : ActionRowBuilder < ButtonBuilder > [ ] {
228+ return [
229+ new ActionRowBuilder < ButtonBuilder > ( ) . addComponents (
230+ new ButtonBuilder ( )
231+ . setCustomId ( `ode:launcher:setting:${ channelId } ` )
232+ . setStyle ( ButtonStyle . Secondary )
233+ . setLabel ( "General setting" ) ,
234+ new ButtonBuilder ( )
235+ . setCustomId ( `ode:launcher:channel:${ channelId } ` )
236+ . setStyle ( ButtonStyle . Secondary )
237+ . setLabel ( "Channel setting" ) ,
238+ new ButtonBuilder ( )
239+ . setCustomId ( `ode:launcher:gh:${ channelId } ` )
240+ . setStyle ( ButtonStyle . Secondary )
241+ . setLabel ( "GitHub info" )
242+ ) ,
243+ buildSettingsLinkRow ( ) ,
244+ ] ;
245+ }
246+
247+ function buildLauncherReplyPayload ( params : {
248+ command : LauncherCommand ;
249+ userId : string ;
250+ channelId : string ;
251+ } ) : { content : string ; components : ActionRowBuilder < ButtonBuilder > [ ] } {
252+ const { command, userId, channelId } = params ;
253+ if ( command === "setting" ) {
254+ return {
255+ content : "Choose which settings page to open." ,
256+ components : buildSettingsChooserRows ( channelId ) ,
257+ } ;
258+ }
259+
260+ return {
261+ content : buildLauncherCommandText ( { command, userId, channelId } ) ,
262+ components : [ buildSettingsLinkRow ( ) ] ,
263+ } ;
264+ }
265+
266+ async function handleLauncherButtonInteraction ( interaction : any ) : Promise < void > {
267+ const customId = String ( interaction . customId ?? "" ) ;
268+ if ( ! customId . startsWith ( "ode:launcher:" ) ) return ;
269+
270+ const [ , , commandRaw , channelIdRaw ] = customId . split ( ":" , 4 ) ;
271+ const command = commandRaw as LauncherCommand ;
272+ if ( ! [ "setting" , "channel" , "gh" ] . includes ( command ) ) return ;
273+
274+ const channelId = channelIdRaw || getResolvedChannelId ( interaction ) ;
275+ const payload = buildLauncherReplyPayload ( {
276+ command,
277+ userId : interaction . user . id ,
278+ channelId,
279+ } ) ;
280+
281+ if ( interaction . deferred || interaction . replied ) {
282+ await interaction . followUp ( { ...payload , ephemeral : true } ) ;
283+ return ;
284+ }
285+
286+ await interaction . reply ( { ...payload , ephemeral : true } ) ;
287+ }
288+
201289async function registerDiscordCommands ( client : Client ) : Promise < void > {
202290 try {
203291 const guilds = await client . guilds . fetch ( ) ;
@@ -325,19 +413,25 @@ export async function startDiscordRuntime(reason: string): Promise<boolean> {
325413
326414 client . on ( "interactionCreate" , async ( interaction : any ) => {
327415 try {
416+ if ( interaction . isButton && interaction . isButton ( ) ) {
417+ await handleLauncherButtonInteraction ( interaction ) ;
418+ return ;
419+ }
420+
328421 if ( ! interaction . isChatInputCommand || ! interaction . isChatInputCommand ( ) ) return ;
329422 const commandName = String ( interaction . commandName || "" ) . toLowerCase ( ) ;
330423 if ( ! [ "setting" , "channel" , "gh" ] . includes ( commandName ) ) return ;
331424
332- const channel = interaction . channel ;
333- const resolvedChannelId = channel ?. isThread ?.( ) ? ( channel . parentId ?? interaction . channelId ) : interaction . channelId ;
334- const text = buildLauncherCommandText ( {
335- command : commandName as "setting" | "channel" | "gh" ,
425+ const payload = buildLauncherReplyPayload ( {
426+ command : commandName as LauncherCommand ,
336427 userId : interaction . user . id ,
337- channelId : resolvedChannelId ,
428+ channelId : getResolvedChannelId ( interaction ) ,
338429 } ) ;
339430
340- await interaction . reply ( { content : text , ephemeral : true } ) ;
431+ await interaction . reply ( {
432+ ...payload ,
433+ ephemeral : true ,
434+ } ) ;
341435 } catch ( error ) {
342436 log . error ( "Discord interaction handler failed" , { error : String ( error ) } ) ;
343437 }
0 commit comments