@@ -12,15 +12,16 @@ import { FileType } from '../../../../platform/filesystem/common/fileTypes';
12
12
import { ILogService } from '../../../../platform/log/common/logService' ;
13
13
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService' ;
14
14
import { createServiceIdentifier } from '../../../../util/common/services' ;
15
+ import { CancellationError } from '../../../../util/vs/base/common/errors' ;
15
16
import { ResourceMap , ResourceSet } from '../../../../util/vs/base/common/map' ;
16
17
import { isEqualOrParent } from '../../../../util/vs/base/common/resources' ;
17
18
import { URI } from '../../../../util/vs/base/common/uri' ;
18
- import { CancellationError } from '../../../../util/vs/base/common/errors' ;
19
19
20
20
type RawStoredSDKMessage = SDKMessage & {
21
21
readonly parentUuid : string | null ;
22
22
readonly sessionId : string ;
23
23
readonly timestamp : string ;
24
+ readonly isMeta ?: boolean ;
24
25
}
25
26
interface SummaryEntry {
26
27
readonly type : 'summary' ;
@@ -35,8 +36,16 @@ type StoredSDKMessage = SDKMessage & {
35
36
readonly timestamp : Date ;
36
37
}
37
38
39
+ interface ParsedSessionMessage {
40
+ readonly raw : RawStoredSDKMessage ;
41
+ readonly isMeta : boolean ;
42
+ }
43
+
38
44
export const IClaudeCodeSessionService = createServiceIdentifier < IClaudeCodeSessionService > ( 'IClaudeCodeSessionService' ) ;
39
45
46
+ /**
47
+ * Service to load and manage Claude Code chat sessions from disk.
48
+ */
40
49
export interface IClaudeCodeSessionService {
41
50
readonly _serviceBrand : undefined ;
42
51
getAllSessions ( token : CancellationToken ) : Promise < readonly IClaudeCodeSession [ ] > ;
@@ -290,7 +299,6 @@ export class ClaudeCodeSessionService implements IClaudeCodeSessionService {
290
299
}
291
300
292
301
private async _getMessagesFromSession ( fileUri : URI , token : CancellationToken ) : Promise < { messages : Map < string , StoredSDKMessage > ; summaries : Map < string , SummaryEntry > } > {
293
- const messages = new Map < string , StoredSDKMessage > ( ) ;
294
302
const summaries = new Map < string , SummaryEntry > ( ) ;
295
303
try {
296
304
// Read and parse the JSONL file
@@ -303,18 +311,30 @@ export class ClaudeCodeSessionService implements IClaudeCodeSessionService {
303
311
304
312
// Parse JSONL content line by line
305
313
const lines = text . trim ( ) . split ( '\n' ) . filter ( line => line . trim ( ) ) ;
314
+ const rawMessages = new Map < string , ParsedSessionMessage > ( ) ;
306
315
307
316
// Parse each line and build message map
308
317
for ( const line of lines ) {
309
318
try {
310
319
const entry = JSON . parse ( line ) as ClaudeSessionFileEntry ;
311
320
312
321
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 ;
317
326
}
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
+ } ) ;
318
338
} else if ( 'summary' in entry && entry . summary && ! entry . summary . toLowerCase ( ) . startsWith ( 'api error: 401' ) && ! entry . summary . toLowerCase ( ) . startsWith ( 'invalid api key' ) ) {
319
339
const summaryEntry = entry as SummaryEntry ;
320
340
const uuid = summaryEntry . leafUuid ;
@@ -326,6 +346,8 @@ export class ClaudeCodeSessionService implements IClaudeCodeSessionService {
326
346
this . _logService . warn ( `Failed to parse line in ${ fileUri } : ${ line } - ${ parseError } ` ) ;
327
347
}
328
348
}
349
+
350
+ const messages = this . _reviveStoredMessages ( rawMessages ) ;
329
351
return { messages, summaries } ;
330
352
} catch ( e ) {
331
353
this . _logService . error ( e , `[ClaudeChatSessionItemProvider] Failed to load session: ${ fileUri } ` ) ;
@@ -380,22 +402,100 @@ export class ClaudeCodeSessionService implements IClaudeCodeSessionService {
380
402
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 ( ) ;
381
403
}
382
404
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
+
383
479
/**
384
480
* Strip attachments from message content, handling both string and array formats
385
481
*/
386
482
private _stripAttachmentsFromMessageContent ( content : Anthropic . MessageParam [ 'content' ] ) : string | Anthropic . ContentBlockParam [ ] {
387
483
if ( typeof content === 'string' ) {
388
- return this . _stripAttachments ( content ) ;
484
+ const withoutAttachments = this . _stripAttachments ( content ) ;
485
+ return this . _normalizeCommandContent ( withoutAttachments ) ;
389
486
} else if ( Array . isArray ( content ) ) {
390
- return content . map ( block => {
487
+ const processedBlocks = content . map ( block => {
391
488
if ( block . type === 'text' ) {
489
+ const textBlock = block ;
490
+ const cleanedText = this . _normalizeCommandContent ( this . _stripAttachments ( textBlock . text ) ) ;
392
491
return {
393
492
...block ,
394
- text : this . _stripAttachments ( ( block as Anthropic . TextBlockParam ) . text )
493
+ text : cleanedText
395
494
} ;
396
495
}
397
496
return block ;
398
- } ) ;
497
+ } ) . filter ( block => block . type !== 'text' || block . text . trim ( ) . length > 0 ) ;
498
+ return processedBlocks ;
399
499
}
400
500
return content ;
401
501
}
0 commit comments