@@ -298,6 +298,25 @@ const slack = async (input: PluginInput): Promise<Hooks> => {
298298 } )
299299 }
300300
301+ // ------------------------------------------------------------------
302+ // Set typing indicator ("is thinking...") in Slack thread
303+ // ------------------------------------------------------------------
304+ // Uses assistant.threads.setStatus which shows "<Bot Name> is thinking..."
305+ // in the thread. The status auto-clears when the bot posts a message.
306+ // Falls back silently if the API is unavailable (e.g., missing scope).
307+
308+ async function setTypingStatus ( channel : string , thread : string , status = "is thinking..." ) {
309+ try {
310+ await web . apiCall ( "assistant.threads.setStatus" , {
311+ channel_id : channel ,
312+ thread_ts : thread ,
313+ status,
314+ } )
315+ } catch {
316+ // Best-effort — don't fail the message flow if status API is unavailable
317+ }
318+ }
319+
301320 // ------------------------------------------------------------------
302321 // Get or create an OpenCode session for a Slack thread
303322 // ------------------------------------------------------------------
@@ -346,11 +365,12 @@ const slack = async (input: PluginInput): Promise<Hooks> => {
346365 evictOldestSession ( )
347366 }
348367
349- // Share session link in thread
368+ // Log session share link (not posted to Slack — too low-level for users).
369+ // Visible in OpenCode server logs: look for "[slack-plugin] Session share:"
350370 try {
351371 const shareResult = await client . session . share ( { path : { id : result . data . id } } )
352372 if ( ! shareResult . error && shareResult . data ?. share ?. url ) {
353- await postToThread ( channel , thread , ` Session: ${ shareResult . data . share . url } `)
373+ console . log ( ` ${ LOG_PREFIX } Session share : ${ shareResult . data . share . url } (thread: ${ key } ) `)
354374 }
355375 } catch {
356376 // Share is optional
@@ -484,6 +504,9 @@ const slack = async (input: PluginInput): Promise<Hooks> => {
484504
485505 session . lastActive = Date . now ( )
486506
507+ // Show typing indicator while processing
508+ setTypingStatus ( channel , thread )
509+
487510 // Enqueue the prompt to serialize concurrent messages in the same session.
488511 // OpenCode silently drops prompt() calls on a busy session (the message is
489512 // written to DB but runLoop never starts), so we must serialize here.
@@ -499,13 +522,24 @@ const slack = async (input: PluginInput): Promise<Hooks> => {
499522 return
500523 }
501524
502- const responseText =
503- ( result . data as any ) . parts
504- ?. filter ( ( p : any ) => p . type === "text" )
505- . map ( ( p : any ) => ( "text" in p ? p . text : "" ) )
506- . join ( "\n" ) || "I received your message but didn't have a response."
525+ // Extract text from response parts. The response shape is
526+ // { info: AssistantMessage, parts: Part[] } where Part can be
527+ // { type: "text", text: string } or tool/step parts.
528+ const data = result . data as any
529+ const parts = data ?. parts ?? [ ]
530+ const textParts = parts
531+ . filter ( ( p : any ) => p . type === "text" && p . text )
532+ . map ( ( p : any ) => p . text )
533+
534+ if ( textParts . length === 0 ) {
535+ // No text in response — log the raw data for debugging but don't
536+ // show a confusing fallback message to the user. The session.idle
537+ // event or subsequent messages will follow.
538+ console . warn ( `${ LOG_PREFIX } Empty text response for session ${ session . sessionId } :` , JSON . stringify ( data ) . slice ( 0 , 500 ) )
539+ return
540+ }
507541
508- await postToThread ( channel , thread , responseText )
542+ await postToThread ( channel , thread , textParts . join ( "\n" ) )
509543 } )
510544 }
511545
@@ -650,27 +684,28 @@ const slack = async (input: PluginInput): Promise<Hooks> => {
650684 event : async ( { event } ) => {
651685 const evt = event as any
652686
653- // Forward tool completion updates to the relevant Slack thread
687+ // Log tool completion events (not posted to Slack — too noisy for users).
688+ // Visible in OpenCode server logs: look for "[slack-plugin] Tool:"
654689 if ( evt . type === "message.part.updated" ) {
655690 const part = evt . properties ?. part
656691 if ( part ?. type === "tool" && part . state ?. status === "completed" && part . sessionID ) {
657692 const session = findSessionByOpenCodeId ( part . sessionID )
658693 if ( session ) {
659- const toolMsg = `*${ part . tool } * — ${ part . state . title || "completed" } `
660- postToThread ( session . channel , session . thread , toolMsg ) . catch ( ( ) => { } )
694+ console . log ( `${ LOG_PREFIX } Tool: ${ part . tool } — ${ part . state . title || "completed" } (session: ${ session . sessionId } )` )
695+ // Refresh typing status so the user knows the bot is still working
696+ setTypingStatus ( session . channel , session . thread , "is working..." )
661697 }
662698 }
663699 }
664700
665- // Notify Slack thread when the session finishes processing (idle).
666- // This gives users a clear signal that the bot is done and ready
667- // for the next message, which is especially useful for long tasks.
701+ // Log session idle events (not posted to Slack).
702+ // Visible in OpenCode server logs: look for "[slack-plugin] Session idle:"
668703 if ( evt . type === "session.idle" ) {
669704 const sessionId = evt . properties ?. sessionID ?? evt . properties ?. id
670705 if ( sessionId ) {
671706 const session = findSessionByOpenCodeId ( sessionId )
672707 if ( session ) {
673- postToThread ( session . channel , session . thread , "_Session idle — ready for next message._" ) . catch ( ( ) => { } )
708+ console . log ( ` ${ LOG_PREFIX } Session idle: ${ session . sessionId } ` )
674709 }
675710 }
676711 }
0 commit comments