@@ -3,6 +3,7 @@ import Foundation
33
44enum ChatWindowResolutionMethod {
55 case existingWindow
6+ case openedViaChatList
67 case openedViaSearch
78}
89
@@ -13,6 +14,10 @@ struct ChatWindowResolution {
1314 var openedViaSearch : Bool {
1415 method == . openedViaSearch
1516 }
17+
18+ var openedTransiently : Bool {
19+ method != . existingWindow
20+ }
1621}
1722
1823private enum ChatWindowFailureCode : String {
@@ -43,6 +48,9 @@ private struct SearchCandidate {
4348}
4449
4550struct ChatWindowResolver {
51+ private static let minimumReadableWindowSize = CGSize ( width: 760 , height: 900 )
52+ private static let maximumAutomaticWindowSize = CGSize ( width: 1200 , height: 1000 )
53+
4654 private let kakao : KakaoTalkApp
4755 private let runner : AXActionRunner
4856 private let useCache : Bool
@@ -64,11 +72,42 @@ struct ChatWindowResolver {
6472 let usableWindow = try requireUsableWindow ( )
6573
6674 if let existingWindow = findMatchingChatWindow ( in: kakao. windows, query: query) {
75+ standardizeReadableWindow ( existingWindow, label: " existing chat window " )
6776 return ChatWindowResolution ( window: existingWindow, method: . existingWindow)
6877 }
6978
7079 let searchWindow = selectSearchWindow ( fallback: usableWindow)
80+ standardizeReadableWindow ( searchWindow, label: " search root window " )
7181 let chatWindow = try openChatViaSearch ( query: query, in: searchWindow, fallbackWindow: usableWindow)
82+ standardizeReadableWindow ( chatWindow, label: " opened chat window " )
83+ return ChatWindowResolution ( window: chatWindow, method: . openedViaSearch)
84+ }
85+
86+ func resolve( chatID: String ) throws -> ChatWindowResolution {
87+ guard let record = ChatIdentityRegistryStore . shared. record ( for: chatID) else {
88+ throw KakaoTalkError . elementNotFound ( " Unknown chat_id ' \( chatID) '. Run 'kmsg chats' first to refresh the local registry. " )
89+ }
90+
91+ let usableWindow = try requireUsableWindow ( )
92+ let query = record. displayName
93+
94+ if let existingWindow = findMatchingChatWindow ( in: kakao. windows, query: query) {
95+ standardizeReadableWindow ( existingWindow, label: " existing chat window " )
96+ return ChatWindowResolution ( window: existingWindow, method: . existingWindow)
97+ }
98+
99+ if let chatListWindow = kakao. chatListWindow,
100+ let chatWindow = openChatListRow ( chatID: chatID, query: query, in: chatListWindow, fallbackWindow: usableWindow)
101+ {
102+ standardizeReadableWindow ( chatWindow, label: " opened chat window " )
103+ return ChatWindowResolution ( window: chatWindow, method: . openedViaChatList)
104+ }
105+
106+ runner. log ( " chat_id: falling back to search for ' \( query) ' " )
107+ let searchWindow = selectSearchWindow ( fallback: usableWindow)
108+ standardizeReadableWindow ( searchWindow, label: " search root window " )
109+ let chatWindow = try openChatViaSearch ( query: query, in: searchWindow, fallbackWindow: usableWindow)
110+ standardizeReadableWindow ( chatWindow, label: " opened chat window " )
72111 return ChatWindowResolution ( window: chatWindow, method: . openedViaSearch)
73112 }
74113
@@ -229,6 +268,110 @@ struct ChatWindowResolver {
229268 throw KakaoTalkError . windowNotFound ( " [ \( ChatWindowFailureCode . windowNotReady. rawValue) ] Chat window for ' \( query) ' did not open " )
230269 }
231270
271+ private func openChatListRow( chatID: String , query: String , in chatListWindow: UIElement , fallbackWindow: UIElement ) -> UIElement ? {
272+ runner. log ( " chat_id: scanning chat list rows " )
273+ standardizeReadableWindow ( chatListWindow, label: " chat list window " )
274+ let scanner = ChatListScanner ( )
275+ let snapshots = scanner. scan ( in: chatListWindow, limit: 200 , trace: { message in
276+ runner. log ( message)
277+ } )
278+ guard !snapshots. isEmpty else {
279+ runner. log ( " chat_id: chat list scan returned no rows " )
280+ return nil
281+ }
282+
283+ let registry = ChatIdentityRegistryStore . shared
284+ let assignedIDs = registry. assignChatIDs ( for: snapshots. map ( \. discovery) )
285+ guard let matchIndex = assignedIDs. firstIndex ( of: chatID) else {
286+ runner. log ( " chat_id: no visible chat row matched \( chatID) " )
287+ return nil
288+ }
289+
290+ let row = snapshots [ matchIndex] . element
291+ runner. log ( " chat_id: matched row title=' \( snapshots [ matchIndex] . discovery. title) ' " )
292+ kakao. activate ( )
293+ _ = tryRaiseWindow ( chatListWindow)
294+
295+ if triggerChatListRowOpen ( row) {
296+ if let window = waitForOpenedChatWindow ( query: query, fallbackWindow: fallbackWindow) {
297+ return window
298+ }
299+ }
300+
301+ runner. log ( " chat_id: matched row did not open a chat window " )
302+ return nil
303+ }
304+
305+ private func triggerChatListRowOpen( _ row: UIElement ) -> Bool {
306+ if tryActivateSearchResult ( row, label: " chat list row " ) {
307+ return true
308+ }
309+
310+ let selected = trySelectSearchResult ( row, label: " chat list row " )
311+ if !selected, let parent = row. parent, trySelectSearchResult ( parent, label: " chat list row.parent " ) {
312+ runner. pressEnterKey ( )
313+ return true
314+ }
315+
316+ if selected {
317+ runner. pressEnterKey ( )
318+ return true
319+ }
320+
321+ return false
322+ }
323+
324+ private func standardizeReadableWindow( _ window: UIElement , label: String ) {
325+ kakao. activate ( )
326+ _ = tryRaiseWindow ( window)
327+
328+ guard let currentSize = window. size else {
329+ runner. log ( " \( label) : size unavailable; skipping resize " )
330+ return
331+ }
332+ let currentPosition = window. position
333+
334+ let targetSize = readableTargetSize ( for: currentSize)
335+ guard targetSize != currentSize else {
336+ runner. log ( " \( label) : size already readable \( Int ( currentSize. width) ) x \( Int ( currentSize. height) ) " )
337+ return
338+ }
339+
340+ do {
341+ try window. setSize ( targetSize)
342+ if let currentPosition {
343+ try ? window. setPosition ( currentPosition)
344+ }
345+ runner. log ( " \( label) : resized to \( Int ( targetSize. width) ) x \( Int ( targetSize. height) ) " )
346+ Thread . sleep ( forTimeInterval: 0.08 )
347+ } catch {
348+ runner. log ( " \( label) : resize failed ( \( error) ) " )
349+ }
350+ }
351+
352+ private func readableTargetSize( for currentSize: CGSize ) -> CGSize {
353+ CGSize (
354+ width: readableTargetDimension (
355+ current: currentSize. width,
356+ minimum: Self . minimumReadableWindowSize. width,
357+ automaticMaximum: Self . maximumAutomaticWindowSize. width
358+ ) ,
359+ height: readableTargetDimension (
360+ current: currentSize. height,
361+ minimum: Self . minimumReadableWindowSize. height,
362+ automaticMaximum: Self . maximumAutomaticWindowSize. height
363+ )
364+ )
365+ }
366+
367+ private func readableTargetDimension( current: CGFloat , minimum: CGFloat , automaticMaximum: CGFloat ) -> CGFloat {
368+ if current >= automaticMaximum {
369+ return current
370+ }
371+
372+ return max ( current, minimum)
373+ }
374+
232375 private func resolveCachedElement(
233376 slot: AXPathSlot ,
234377 root: UIElement ,
0 commit comments