11import type { Plugin } from "@opencode-ai/plugin"
2- import type {
3- Event ,
4- EventSessionCreated ,
5- EventSessionIdle ,
6- EventMessageUpdated ,
7- EventPermissionUpdated ,
8- } from "@opencode-ai/sdk"
2+ import type { Event , Part , Permission } from "@opencode-ai/sdk"
93
104import { buildPayload } from "./payload"
115import { warpNotify } from "./notify"
@@ -18,15 +12,45 @@ function truncate(str: string, maxLen: number): string {
1812 return str . slice ( 0 , maxLen - 3 ) + "..."
1913}
2014
21- function extractTextFromParts (
22- parts : Array < { type ?: string ; text ?: string } > ,
23- ) : string {
15+ function extractTextFromParts ( parts : Part [ ] ) : string {
2416 return parts
25- . filter ( ( p ) => p . type === "text" && p . text )
26- . map ( ( p ) => p . text ! )
17+ . filter ( ( p ) : p is Part & { type : "text" ; text : string } =>
18+ p . type === "text" && "text" in p && Boolean ( p . text ) ,
19+ )
20+ . map ( ( p ) => p . text )
2721 . join ( " " )
2822}
2923
24+ function sendPermissionNotification ( perm : Permission , cwd : string ) : void {
25+ const sessionId = perm . sessionID
26+ const toolName = perm . type || "unknown"
27+ const metadata = perm . metadata || { }
28+
29+ let toolPreview = ""
30+ if ( typeof metadata . command === "string" ) {
31+ toolPreview = metadata . command
32+ } else if ( typeof metadata . file_path === "string" ) {
33+ toolPreview = metadata . file_path as string
34+ } else if ( typeof metadata . filePath === "string" ) {
35+ toolPreview = metadata . filePath as string
36+ } else {
37+ const raw = JSON . stringify ( metadata )
38+ toolPreview = raw . slice ( 0 , 80 )
39+ }
40+
41+ let summary = `Wants to run ${ toolName } `
42+ if ( toolPreview ) {
43+ summary += `: ${ truncate ( toolPreview , 120 ) } `
44+ }
45+
46+ const body = buildPayload ( "permission_request" , sessionId , cwd , {
47+ summary,
48+ tool_name : toolName ,
49+ tool_input : metadata ,
50+ } )
51+ warpNotify ( NOTIFICATION_TITLE , body )
52+ }
53+
3054export const WarpPlugin : Plugin = async ( { client, directory } ) => {
3155 await client . app . log ( {
3256 body : {
@@ -40,133 +64,103 @@ export const WarpPlugin: Plugin = async ({ client, directory }) => {
4064 event : async ( { event } : { event : Event } ) => {
4165 const cwd = directory || ""
4266
43- if ( event . type === "session.created" ) {
44- const ev = event as EventSessionCreated
45- const sessionId = ev . properties . info . id
46- const body = buildPayload ( "session_start" , sessionId , cwd , {
47- plugin_version : PLUGIN_VERSION ,
48- } )
49- warpNotify ( NOTIFICATION_TITLE , body )
50- return
51- }
52-
53- if ( event . type === "session.idle" ) {
54- const ev = event as EventSessionIdle
55- const sessionId = ev . properties . sessionID
56-
57- // Fetch the conversation to extract last query and response
58- // (port of on-stop.sh transcript parsing)
59- let query = ""
60- let response = ""
61-
62- if ( sessionId ) {
63- try {
64- const result = await client . session . messages ( {
65- path : { id : sessionId } ,
66- } )
67- const messages = result . data as
68- | Array < {
69- info : { role ?: string }
70- parts : Array < { type ?: string ; text ?: string } >
71- } >
72- | undefined
73-
74- if ( messages ) {
75- const lastUser = [ ...messages ]
76- . reverse ( )
77- . find ( ( m ) => m . info . role === "user" )
78- if ( lastUser ) {
79- query = extractTextFromParts ( lastUser . parts )
80- }
67+ switch ( event . type ) {
68+ case "session.created" : {
69+ const sessionId = event . properties . info . id
70+ const body = buildPayload ( "session_start" , sessionId , cwd , {
71+ plugin_version : PLUGIN_VERSION ,
72+ } )
73+ warpNotify ( NOTIFICATION_TITLE , body )
74+ return
75+ }
8176
82- const lastAssistant = [ ...messages ]
83- . reverse ( )
84- . find ( ( m ) => m . info . role === "assistant" )
85- if ( lastAssistant ) {
86- response = extractTextFromParts ( lastAssistant . parts )
77+ case "session.idle" : {
78+ const sessionId = event . properties . sessionID
79+
80+ // Fetch the conversation to extract last query and response
81+ // (port of on-stop.sh transcript parsing)
82+ let query = ""
83+ let response = ""
84+
85+ if ( sessionId ) {
86+ try {
87+ const result = await client . session . messages ( {
88+ path : { id : sessionId } ,
89+ } )
90+ const messages = result . data
91+
92+ if ( messages ) {
93+ const reversed = [ ...messages ] . reverse ( )
94+
95+ const lastUser = reversed . find (
96+ ( m ) => m . info . role === "user" ,
97+ )
98+ if ( lastUser ) {
99+ query = extractTextFromParts ( lastUser . parts )
100+ }
101+
102+ const lastAssistant = reversed . find (
103+ ( m ) => m . info . role === "assistant" ,
104+ )
105+ if ( lastAssistant ) {
106+ response = extractTextFromParts ( lastAssistant . parts )
107+ }
87108 }
109+ } catch {
110+ // If we can't fetch messages, send the notification without query/response
88111 }
89- } catch {
90- // If we can't fetch messages, send the notification without query/response
91112 }
92- }
93-
94- const body = buildPayload ( "stop" , sessionId , cwd , {
95- query : truncate ( query , 200 ) ,
96- response : truncate ( response , 200 ) ,
97- transcript_path : "" ,
98- } )
99- warpNotify ( NOTIFICATION_TITLE , body )
100- return
101- }
102113
103- if ( event . type === "permission.updated" || ( event as any ) . type === "permission.asked" ) {
104- const ev = event as EventPermissionUpdated
105- const perm = ev . properties
106- const sessionId = perm . sessionID
107- const toolName = perm . type || "unknown"
108- const metadata = perm . metadata || { }
109-
110- let toolPreview = ""
111- if ( typeof metadata . command === "string" ) {
112- toolPreview = metadata . command
113- } else if ( typeof metadata . file_path === "string" ) {
114- toolPreview = metadata . file_path as string
115- } else if ( typeof metadata . filePath === "string" ) {
116- toolPreview = metadata . filePath as string
117- } else {
118- const raw = JSON . stringify ( metadata )
119- toolPreview = raw . slice ( 0 , 80 )
114+ const body = buildPayload ( "stop" , sessionId , cwd , {
115+ query : truncate ( query , 200 ) ,
116+ response : truncate ( response , 200 ) ,
117+ transcript_path : "" ,
118+ } )
119+ warpNotify ( NOTIFICATION_TITLE , body )
120+ return
120121 }
121122
122- let summary = `Wants to run ${ toolName } `
123- if ( toolPreview ) {
124- summary += `: ${ truncate ( toolPreview , 120 ) } `
123+ case "permission.updated" : {
124+ sendPermissionNotification ( event . properties , cwd )
125+ return
125126 }
126127
127- const body = buildPayload ( "permission_request" , sessionId , cwd , {
128- summary,
129- tool_name : toolName ,
130- tool_input : metadata ,
131- } )
132- warpNotify ( NOTIFICATION_TITLE , body )
133- return
134- }
128+ case "message.updated" : {
129+ const message = event . properties . info
130+ if ( message . role !== "user" ) return
131+
132+ const sessionId = message . sessionID
135133
136- if ( event . type === "message.updated" ) {
137- const ev = event as EventMessageUpdated
138- const message = ev . properties . info
139- if ( message . role !== "user" ) return
134+ // message.updated doesn't carry parts directly — fetch the message
135+ let queryText = ""
136+ try {
137+ const result = await client . session . message ( {
138+ path : { id : sessionId , messageID : message . id } ,
139+ } )
140+ if ( result . data ) {
141+ queryText = extractTextFromParts ( result . data . parts )
142+ }
143+ } catch {
144+ // Fall back to using summary title if available
145+ queryText = message . summary ?. title ?? ""
146+ }
140147
141- const sessionId = message . sessionID
148+ if ( ! queryText ) return
142149
143- // message.updated doesn't carry parts directly — fetch the message
144- let queryText = ""
145- try {
146- const result = await client . session . message ( {
147- path : { id : sessionId , messageID : message . id } ,
150+ const body = buildPayload ( "prompt_submit" , sessionId , cwd , {
151+ query : truncate ( queryText , 200 ) ,
148152 } )
149- const data = result . data as
150- | {
151- info : { role ?: string }
152- parts : Array < { type ?: string ; text ?: string } >
153- }
154- | undefined
155- if ( data ) {
156- queryText = extractTextFromParts ( data . parts )
157- }
158- } catch {
159- // Fall back to using summary title if available
160- queryText = message . summary ?. title ?? ""
153+ warpNotify ( NOTIFICATION_TITLE , body )
154+ return
161155 }
162156
163- if ( ! queryText ) return
164-
165- const body = buildPayload ( "prompt_submit" , sessionId , cwd , {
166- query : truncate ( queryText , 200 ) ,
167- } )
168- warpNotify ( NOTIFICATION_TITLE , body )
169- return
157+ default : {
158+ // permission.asked is listed in the opencode docs but has no SDK type.
159+ // Handle it with the same logic as permission.updated.
160+ if ( ( event as any ) . type === "permission.asked" ) {
161+ sendPermissionNotification ( ( event as any ) . properties , cwd )
162+ }
163+ }
170164 }
171165 } ,
172166
0 commit comments