@@ -7,18 +7,23 @@ struct TranscriptMessage: Encodable, Equatable, Sendable {
77 let body : String
88 let isSystem : Bool
99 let logicalTimestamp : Date ?
10+ /// Calendar date of the message ("YYYY-MM-DD"), read from the time
11+ /// label's AXHelp tooltip. nil when the tooltip was unavailable.
12+ let date : String ?
1013
1114 enum CodingKeys : String , CodingKey {
1215 case author
1316 case timeRaw = " time_raw "
1417 case body
18+ case date
1519 }
1620
1721 func encode( to encoder: Encoder ) throws {
1822 var container = encoder. container ( keyedBy: CodingKeys . self)
1923 try container. encode ( author ?? " (me) " , forKey: . author)
2024 try container. encodeIfPresent ( timeRaw, forKey: . timeRaw)
2125 try container. encode ( body, forKey: . body)
26+ try container. encodeIfPresent ( date, forKey: . date)
2227 }
2328}
2429
@@ -202,6 +207,9 @@ struct KakaoTalkTranscriptReader {
202207 var leftAnchorAuthor : String ?
203208 var leftAnchorTimeRaw : String ?
204209 var currentDateAnchor : Date ?
210+ // AXHelp tooltip date ("YYYY-MM-DD") carried forward so consecutive
211+ // messages that share a single time label inherit the same day.
212+ var lastKnownDate : String ?
205213
206214 for (offset, analysis) in analyses. enumerated ( ) {
207215 let side = analysis. side
@@ -242,13 +250,19 @@ struct KakaoTalkTranscriptReader {
242250 continue
243251 }
244252
253+ if let axHelpDate = analysis. axHelpDate {
254+ lastKnownDate = axHelpDate
255+ }
256+ let resolvedDate = analysis. axHelpDate ?? lastKnownDate
257+
245258 if analysis. isSystemLikeRow {
246259 let message = TranscriptMessage (
247260 author: nil ,
248261 timeRaw: analysis. timeRaw,
249262 body: bodyCandidate. body,
250263 isSystem: true ,
251- logicalTimestamp: currentDateAnchor
264+ logicalTimestamp: currentDateAnchor,
265+ date: resolvedDate
252266 )
253267 messages. append ( message)
254268 continue
@@ -283,7 +297,8 @@ struct KakaoTalkTranscriptReader {
283297 for: resolvedTime,
284298 dateAnchor: currentDateAnchor,
285299 referenceDate: referenceDate
286- )
300+ ) ,
301+ date: resolvedDate
287302 )
288303 messages. append ( message)
289304 if selectedLogs < 10 {
@@ -322,6 +337,7 @@ struct KakaoTalkTranscriptReader {
322337 var metadataTokensBuffer : [ String ] = [ ]
323338 var buttonTitlesBuffer : [ String ] = [ ]
324339 var imageFrames : [ CGRect ] = [ ]
340+ var rowHelpDate : String ?
325341
326342 for container in containers {
327343 var textAreas : [ UIElement ] = [ ]
@@ -369,6 +385,9 @@ struct KakaoTalkTranscriptReader {
369385 }
370386
371387 for staticText in staticTexts {
388+ if rowHelpDate == nil , let help = staticText. helpText, let parsed = Self . parseHelpDate ( help) {
389+ rowHelpDate = parsed
390+ }
372391 let normalized = normalizeBodyText ( staticText. stringValue)
373392 guard !normalized. isEmpty else { continue }
374393 metadataTokensBuffer. append ( contentsOf: metadataTokens ( from: normalized) )
@@ -436,7 +455,8 @@ struct KakaoTalkTranscriptReader {
436455 timeRaw: metadata. timeRaw,
437456 side: side,
438457 rowFrame: cachedRowFrame,
439- isSystemLikeRow: systemLikeRow
458+ isSystemLikeRow: systemLikeRow,
459+ axHelpDate: rowHelpDate
440460 )
441461 }
442462
@@ -469,7 +489,8 @@ struct KakaoTalkTranscriptReader {
469489 for: metadata. timeRaw,
470490 dateAnchor: nil ,
471491 referenceDate: referenceDate
472- )
492+ ) ,
493+ date: row. flatMap { axHelpDate ( in: $0) }
473494 )
474495 )
475496 }
@@ -486,7 +507,8 @@ struct KakaoTalkTranscriptReader {
486507 timeRaw: nil ,
487508 body: title,
488509 isSystem: false ,
489- logicalTimestamp: nil
510+ logicalTimestamp: nil ,
511+ date: nil
490512 )
491513 )
492514 }
@@ -850,6 +872,35 @@ struct KakaoTalkTranscriptReader {
850872 return nil
851873 }
852874
875+ /// Parse the AXHelp tooltip KakaoTalk attaches to a message's time label,
876+ /// e.g. "2026. 6. 2." -> "2026-06-02".
877+ private static func parseHelpDate( _ text: String ) -> String ? {
878+ let trimmed = text. trimmingCharacters ( in: . whitespacesAndNewlines)
879+ guard let match = trimmed. range (
880+ of: #"(\d{4})\.\s*(\d{1,2})\.\s*(\d{1,2})\.?"# ,
881+ options: . regularExpression
882+ ) else {
883+ return nil
884+ }
885+ let numbers = trimmed [ match]
886+ . split ( whereSeparator: { !$0. isNumber } )
887+ . compactMap { Int ( $0) }
888+ guard numbers. count >= 3 else { return nil }
889+ return String ( format: " %04d-%02d-%02d " , numbers [ 0 ] , numbers [ 1 ] , numbers [ 2 ] )
890+ }
891+
892+ /// Scan a row's static texts for the first AXHelp date tooltip.
893+ /// Used by the fallback message path, which lacks the per-row analysis.
894+ private func axHelpDate( in row: UIElement ) -> String ? {
895+ let staticTexts = row. findAll ( role: kAXStaticTextRole, limit: 12 , maxNodes: 240 )
896+ for staticText in staticTexts {
897+ if let help = staticText. helpText, let parsed = Self . parseHelpDate ( help) {
898+ return parsed
899+ }
900+ }
901+ return nil
902+ }
903+
853904 private func firstAncestor( of element: UIElement , role: String , maxHops: Int ) -> UIElement ? {
854905 var cursor : UIElement ? = element
855906 var hops = 0
@@ -1017,6 +1068,7 @@ private struct RowAnalysis {
10171068 let side : MessageSide
10181069 let rowFrame : CGRect ?
10191070 let isSystemLikeRow : Bool
1071+ let axHelpDate : String ?
10201072
10211073 var referenceFrame : CGRect ? {
10221074 bodyCandidate? . frame ?? rowFrame
0 commit comments