@@ -12,15 +12,16 @@ import { FileType } from '../../../../platform/filesystem/common/fileTypes';
1212import { ILogService } from '../../../../platform/log/common/logService' ;
1313import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService' ;
1414import { createServiceIdentifier } from '../../../../util/common/services' ;
15+ import { CancellationError } from '../../../../util/vs/base/common/errors' ;
1516import { ResourceMap , ResourceSet } from '../../../../util/vs/base/common/map' ;
1617import { isEqualOrParent } from '../../../../util/vs/base/common/resources' ;
1718import { URI } from '../../../../util/vs/base/common/uri' ;
18- import { CancellationError } from '../../../../util/vs/base/common/errors' ;
1919
2020type RawStoredSDKMessage = SDKMessage & {
2121 readonly parentUuid : string | null ;
2222 readonly sessionId : string ;
2323 readonly timestamp : string ;
24+ readonly isMeta ?: boolean ;
2425}
2526interface SummaryEntry {
2627 readonly type : 'summary' ;
@@ -35,8 +36,16 @@ type StoredSDKMessage = SDKMessage & {
3536 readonly timestamp : Date ;
3637}
3738
39+ interface ParsedSessionMessage {
40+ readonly raw : RawStoredSDKMessage ;
41+ readonly isMeta : boolean ;
42+ }
43+
3844export const IClaudeCodeSessionService = createServiceIdentifier < IClaudeCodeSessionService > ( 'IClaudeCodeSessionService' ) ;
3945
46+ /**
47+ * Service to load and manage Claude Code chat sessions from disk.
48+ */
4049export interface IClaudeCodeSessionService {
4150 readonly _serviceBrand : undefined ;
4251 getAllSessions ( token : CancellationToken ) : Promise < readonly IClaudeCodeSession [ ] > ;
@@ -290,7 +299,6 @@ export class ClaudeCodeSessionService implements IClaudeCodeSessionService {
290299 }
291300
292301 private async _getMessagesFromSession ( fileUri : URI , token : CancellationToken ) : Promise < { messages : Map < string , StoredSDKMessage > ; summaries : Map < string , SummaryEntry > } > {
293- const messages = new Map < string , StoredSDKMessage > ( ) ;
294302 const summaries = new Map < string , SummaryEntry > ( ) ;
295303 try {
296304 // Read and parse the JSONL file
@@ -303,18 +311,30 @@ export class ClaudeCodeSessionService implements IClaudeCodeSessionService {
303311
304312 // Parse JSONL content line by line
305313 const lines = text . trim ( ) . split ( '\n' ) . filter ( line => line . trim ( ) ) ;
314+ const rawMessages = new Map < string , ParsedSessionMessage > ( ) ;
306315
307316 // Parse each line and build message map
308317 for ( const line of lines ) {
309318 try {
310319 const entry = JSON . parse ( line ) as ClaudeSessionFileEntry ;
311320
312321 if ( 'uuid' in entry && entry . uuid && 'message' in entry ) {
313- const sdkMessage = this . _reviveStoredSDKMessage ( entry as RawStoredSDKMessage ) ;
314- const uuid = sdkMessage . uuid ;
315- if ( uuid ) {
316- messages . set ( uuid , sdkMessage ) ;
322+ const rawEntry = entry ;
323+ const uuid = rawEntry . uuid ;
324+ if ( ! uuid ) {
325+ continue ;
317326 }
327+
328+ const { isMeta, ...rest } = rawEntry ;
329+ const normalizedRaw = {
330+ ...rest ,
331+ parentUuid : rawEntry . parentUuid ?? null
332+ } as RawStoredSDKMessage ;
333+
334+ rawMessages . set ( uuid , {
335+ raw : normalizedRaw ,
336+ isMeta : Boolean ( isMeta )
337+ } ) ;
318338 } else if ( 'summary' in entry && entry . summary && ! entry . summary . toLowerCase ( ) . startsWith ( 'api error: 401' ) && ! entry . summary . toLowerCase ( ) . startsWith ( 'invalid api key' ) ) {
319339 const summaryEntry = entry as SummaryEntry ;
320340 const uuid = summaryEntry . leafUuid ;
@@ -326,6 +346,8 @@ export class ClaudeCodeSessionService implements IClaudeCodeSessionService {
326346 this . _logService . warn ( `Failed to parse line in ${ fileUri } : ${ line } - ${ parseError } ` ) ;
327347 }
328348 }
349+
350+ const messages = this . _reviveStoredMessages ( rawMessages ) ;
329351 return { messages, summaries } ;
330352 } catch ( e ) {
331353 this . _logService . error ( e , `[ClaudeChatSessionItemProvider] Failed to load session: ${ fileUri } ` ) ;
@@ -380,22 +402,100 @@ export class ClaudeCodeSessionService implements IClaudeCodeSessionService {
380402 return text . replace ( / < s y s t e m - r e m i n d e r > [ \s \S ] * ?< \/ s y s t e m - r e m i n d e r > \s * / g, '' ) . trim ( ) ;
381403 }
382404
405+ private _normalizeCommandContent ( text : string ) : string {
406+ const parsed = this . _extractCommandContent ( text ) ;
407+ if ( parsed !== null ) {
408+ return parsed ;
409+ }
410+ return this . _removeCommandTags ( text ) ;
411+ }
412+
413+ private _extractCommandContent ( text : string ) : string | null {
414+ const commandMessageMatch = / < c o m m a n d - m e s s a g e > ( [ \s \S ] * ?) < \/ c o m m a n d - m e s s a g e > / i. exec ( text ) ;
415+ if ( ! commandMessageMatch ) {
416+ return null ;
417+ }
418+
419+ const commandMessage = commandMessageMatch [ 1 ] ?. trim ( ) ;
420+ return commandMessage ? `/${ commandMessage } ` : null ;
421+ }
422+
423+ private _removeCommandTags ( text : string ) : string {
424+ return text
425+ . replace ( / < c o m m a n d - m e s s a g e > / gi, '' )
426+ . replace ( / < \/ c o m m a n d - m e s s a g e > / gi, '' )
427+ . replace ( / < c o m m a n d - n a m e > / gi, '' )
428+ . replace ( / < \/ c o m m a n d - n a m e > / gi, '' )
429+ . trim ( ) ;
430+ }
431+
432+ private _reviveStoredMessages ( rawMessages : Map < string , ParsedSessionMessage > ) : Map < string , StoredSDKMessage > {
433+ const messages = new Map < string , StoredSDKMessage > ( ) ;
434+
435+ for ( const [ uuid , entry ] of rawMessages ) {
436+ if ( entry . isMeta ) {
437+ continue ;
438+ }
439+
440+ const parentUuid = this . _resolveParentUuid ( entry . raw . parentUuid ?? null , rawMessages ) ;
441+ const revived = this . _reviveStoredSDKMessage ( {
442+ ...entry . raw ,
443+ parentUuid
444+ } ) ;
445+
446+ if ( uuid ) {
447+ messages . set ( uuid , revived ) ;
448+ }
449+ }
450+
451+ return messages ;
452+ }
453+
454+ private _resolveParentUuid ( parentUuid : string | null , rawMessages : Map < string , ParsedSessionMessage > ) : string | null {
455+ let current = parentUuid ;
456+ const visited = new Set < string > ( ) ;
457+
458+ while ( current ) {
459+ if ( visited . has ( current ) ) {
460+ return current ;
461+ }
462+ visited . add ( current ) ;
463+
464+ const candidate = rawMessages . get ( current ) ;
465+ if ( ! candidate ) {
466+ return current ;
467+ }
468+
469+ if ( ! candidate . isMeta ) {
470+ return current ;
471+ }
472+
473+ current = candidate . raw . parentUuid ?? null ;
474+ }
475+
476+ return current ?? null ;
477+ }
478+
383479 /**
384480 * Strip attachments from message content, handling both string and array formats
385481 */
386482 private _stripAttachmentsFromMessageContent ( content : Anthropic . MessageParam [ 'content' ] ) : string | Anthropic . ContentBlockParam [ ] {
387483 if ( typeof content === 'string' ) {
388- return this . _stripAttachments ( content ) ;
484+ const withoutAttachments = this . _stripAttachments ( content ) ;
485+ return this . _normalizeCommandContent ( withoutAttachments ) ;
389486 } else if ( Array . isArray ( content ) ) {
390- return content . map ( block => {
487+ const processedBlocks = content . map ( block => {
391488 if ( block . type === 'text' ) {
489+ const textBlock = block ;
490+ const cleanedText = this . _normalizeCommandContent ( this . _stripAttachments ( textBlock . text ) ) ;
392491 return {
393492 ...block ,
394- text : this . _stripAttachments ( ( block as Anthropic . TextBlockParam ) . text )
493+ text : cleanedText
395494 } ;
396495 }
397496 return block ;
398- } ) ;
497+ } ) . filter ( block => block . type !== 'text' || block . text . trim ( ) . length > 0 ) ;
498+ return processedBlocks ;
399499 }
400500 return content ;
401501 }
0 commit comments