@@ -91,8 +91,12 @@ function generateTabTitle(completedLine) {
9191 return 'PAI Task Done' ;
9292}
9393export const PAIPlugin = async ( { worktree } ) => {
94- let logger = null ;
95- let currentSessionId = null ;
94+ const loggers = new Map ( ) ;
95+ // Track the latest text content for each message (from streaming parts)
96+ // Key: messageID, Value: latest full text from part.text
97+ const messageTextCache = new Map ( ) ;
98+ // Track which messages we've already processed for archival (deduplication)
99+ const processedMessageIds = new Set ( ) ;
96100 // Auto-initialize PAI infrastructure if needed
97101 ensurePAIStructure ( ) ;
98102 // Load CORE skill content from $PAI_DIR/skill/core/SKILL.md
@@ -131,17 +135,33 @@ export const PAIPlugin = async ({ worktree }) => {
131135 const hooks = {
132136 event : async ( { event } ) => {
133137 const anyEvent = event ;
134- // Initialize Logger on session creation
135- if ( event . type === 'session.created' ) {
136- currentSessionId = anyEvent . properties . info . id ;
137- logger = new Logger ( currentSessionId ) ;
138+ // Get Session ID from event (try multiple locations)
139+ const sessionId = anyEvent . properties ?. part ?. sessionID ||
140+ anyEvent . properties ?. info ?. sessionID ||
141+ anyEvent . properties ?. sessionID ||
142+ anyEvent . sessionID ;
143+ if ( ! sessionId )
144+ return ;
145+ // Initialize Logger if needed
146+ if ( ! loggers . has ( sessionId ) ) {
147+ loggers . set ( sessionId , new Logger ( sessionId , worktree ) ) ;
138148 }
139- // Handle generic event logging
140- if ( logger &&
141- event . type !== 'message.part.updated' &&
142- ! shouldSkipEvent ( event , currentSessionId ) ) {
149+ const logger = loggers . get ( sessionId ) ;
150+ // Handle generic event logging (skip streaming parts to reduce noise)
151+ if ( ! shouldSkipEvent ( event , sessionId ) && event . type !== 'message.part.updated' ) {
143152 logger . logOpenCodeEvent ( event ) ;
144153 }
154+ // STREAMING CAPTURE: Cache the latest text from message.part.updated
155+ // The part.text field contains the FULL accumulated text, not a delta
156+ if ( event . type === 'message.part.updated' ) {
157+ const part = anyEvent . properties ?. part ;
158+ const messageId = part ?. messageID ;
159+ const partType = part ?. type ;
160+ // Only cache text parts (not tool parts)
161+ if ( messageId && partType === 'text' && part ?. text ) {
162+ messageTextCache . set ( messageId , part . text ) ;
163+ }
164+ }
145165 // Handle real-time tab title updates (Pre-Tool Use)
146166 if ( anyEvent . type === 'tool.call' ) {
147167 const props = anyEvent . properties ;
@@ -158,39 +178,67 @@ export const PAIPlugin = async ({ worktree }) => {
158178 process . stderr . write ( `\x1b]0;Agent: ${ type } ...\x07` ) ;
159179 }
160180 }
161- // Handle assistant completion (Tab Titles & UOCS )
181+ // Handle assistant message completion (Tab Titles & Artifact Archival )
162182 if ( event . type === 'message.updated' ) {
163183 const info = anyEvent . properties ?. info ;
164184 const role = info ?. role || info ?. author ;
165- if ( role === 'assistant' ) {
166- // Robust content extraction
167- const content = info ?. content || info ?. text || '' ;
168- const contentStr = typeof content === 'string' ? content : '' ;
169- // Look for COMPLETED: line (can be prefaced by 🎯 or just text)
170- const completedMatch = contentStr . match ( / (?: 🎯 \s * ) ? C O M P L E T E D : \s * ( .+ ?) (?: \n | $ ) / i) ;
171- if ( completedMatch ) {
172- const completedLine = completedMatch [ 1 ] . trim ( ) ;
173- // Set Tab Title
174- const tabTitle = generateTabTitle ( completedLine ) ;
175- process . stderr . write ( `\x1b]0;${ tabTitle } \x07` ) ;
176- // UOCS: Process response for artifact generation
177- if ( logger && contentStr ) {
178- await logger . processAssistantMessage ( contentStr ) ;
185+ const messageId = info ?. id ;
186+ if ( role === 'assistant' && messageId ) {
187+ // Get content from our streaming cache first, fallback to info.content
188+ let contentStr = messageTextCache . get ( messageId ) || '' ;
189+ // Fallback: try to get content from the event itself
190+ if ( ! contentStr ) {
191+ const content = info ?. content || info ?. text || '' ;
192+ if ( typeof content === 'string' ) {
193+ contentStr = content ;
194+ }
195+ else if ( Array . isArray ( content ) ) {
196+ contentStr = content
197+ . map ( ( p ) => {
198+ if ( typeof p === 'string' )
199+ return p ;
200+ if ( p ?. text )
201+ return p . text ;
202+ if ( p ?. content )
203+ return p . content ;
204+ return '' ;
205+ } )
206+ . join ( '' ) ;
179207 }
180208 }
209+ // Process if we have content and haven't processed this message yet
210+ if ( contentStr && ! processedMessageIds . has ( messageId ) ) {
211+ processedMessageIds . add ( messageId ) ;
212+ // Look for COMPLETED: line for tab title
213+ const completedMatch = contentStr . match ( / (?: 🎯 \s * ) ? C O M P L E T E D : \s * ( .+ ?) (?: \n | $ ) / i) ;
214+ if ( completedMatch ) {
215+ const completedLine = completedMatch [ 1 ] . trim ( ) ;
216+ const tabTitle = generateTabTitle ( completedLine ) ;
217+ process . stderr . write ( `\x1b]0;${ tabTitle } \x07` ) ;
218+ }
219+ // Archive structured response
220+ await logger . processAssistantMessage ( contentStr , messageId ) ;
221+ // Clean up cache for this message
222+ messageTextCache . delete ( messageId ) ;
223+ }
181224 }
182225 }
183226 // Handle session deletion / end or idle (for one-shot commands)
184227 if ( event . type === 'session.deleted' || event . type === 'session.idle' ) {
185- if ( logger ) {
186- await logger . generateSessionSummary ( ) ;
187- logger . flush ( ) ;
188- }
228+ await logger . generateSessionSummary ( ) ;
229+ logger . flush ( ) ;
230+ loggers . delete ( sessionId ) ;
231+ // Clean up any stale cache entries for this session
232+ // (In practice, messages are cleaned up after processing)
189233 }
190234 } ,
191235 "tool.execute.after" : async ( input , output ) => {
192- if ( logger ) {
193- logger . logToolExecution ( input , output ) ;
236+ const sessionId = input . sessionID ;
237+ if ( sessionId ) {
238+ if ( ! loggers . has ( sessionId ) ) {
239+ loggers . set ( sessionId , new Logger ( sessionId , worktree ) ) ;
240+ }
241+ loggers . get ( sessionId ) . logToolExecution ( input , output ) ;
194242 }
195243 } ,
196244 "permission.ask" : async ( permission ) => {
0 commit comments