
    `gj_                       d Z ddlmZ ddlZddlmZmZmZ eegee	   f   Z
 ej                  d      Zh dZdZd'd(dZd)d	Zd*d
Zd+dZd*dZd,dZd-dZd,dZd.dZd/dZd0dZ	 	 	 	 	 	 	 	 	 	 	 	 	 	 d1dZd2dZd3dZd4dZd5dZd6dZd7dZ d8dZ!d9dZ"	 	 	 	 	 	 	 	 d:dZ# G d d      Z$d;dZ%d<d Z&ddd!d"	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d=d#Z'	 d>d$d!ddd%	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d?d&Z(y)@a"  Authoritative project -> repo -> lane -> session tree builder.

This is the single source of truth for how the desktop sidebar groups sessions
into projects, repos, and lanes. It is pure (all git resolution is injected via
``resolve``) so it can be unit-tested with fixtures and reused by the gateway's
``projects.tree`` / ``projects.project_sessions`` RPCs.

It deliberately mirrors the desktop's former client-side grouping (the old
``workspace-groups.ts``) so the emitted ids and lane keys stay byte-compatible
with the renderer's persisted state (pins, manual ordering, dismissal), which
all key off these exact strings:

  - explicit project id .......... ``p_<hex>`` (from projects.db)
  - auto/discovered project id ... the repo root path
  - repo node id ................. the repo root path
  - main branch lane id .......... ``<repoRoot>::branch::<branch>`` (or ``::branch::``)
  - kanban bucket lane id ........ ``<repoRoot>::kanban``
  - linked worktree lane id ...... the worktree path

The one correctness upgrade over the client version: linked worktrees are folded
under their MAIN repo via a git common-dir probe (injected as ``resolve``),
instead of being treated as separate repos (``git rev-parse --show-toplevel``
returns the worktree's own root, which is why the client double-counted them).
    )annotationsN)AnyCallableOptionalz,^(.*[/\\]\.worktrees)[/\\]t_[0-9a-f]+[/\\]?$>   maintrunkmasterdevelopr   c                4    |  d|xs dj                          S )zGThe one definition of a main-checkout lane id (must match the desktop).
::branch:: )strip)	repo_rootbranchs     K/root/.hermes/venv/lib/python3.12/site-packages/tui_gateway/project_tree.py_branch_lane_idr   -   s!    [
FLb#7#7#9":;;    c                    |  dS )N::kanban )r   s    r   _kanban_lane_idr   2   s    [!!r   c                    t        j                  d| xs dj                  d            D cg c]  }|s|	 c}S c c}w )Nz[/\\]r   z/\)resplitrstrip)pathss     r   	_segmentsr   ;   s2    xx4:2*=*=e*DEK!AKKKs   ;;c                    | xs dj                         }t        t        j                  d|            xs |j	                  d      S )Nr   z^[A-Za-z]:[/\\])\z//)r   boolr   match
startswith)r   values     r   _is_windows_pathr%   ?   s=    ZR E +U34V8H8H8VVr   c                x    t        |       }t        |       r|D cg c]  }|j                          c}S |S c c}w )zPath segments suitable for identity comparisons on any host.

    Windows paths remain case-insensitive even when tests or remote backends run
    on POSIX. Display paths and emitted IDs keep their original spelling.
    )r   r%   casefold)r   segssegments      r   _comparison_segmentsr*   G   s7     T?D7G7Md37G3WSWW3s   7c                6    dj                  t        |             S )z=Canonical comparison key (separator/trailing-slash agnostic)./)joinr*   )r   s    r   	_path_keyr.   Q   s    88(.//r   c                ~    dD ].  }|| v s| j                  |d      \  }}t        |       | | c S  t        |       S )zCanonicalize only the path portion of a lane id.

    Branch labels remain byte-preserved; repo/worktree paths follow platform path
    identity so equivalent Windows spellings do not create duplicate lanes.
    )r   r      )r   r.   )path_or_lanemarkerrootsuffixs       r   	_lane_keyr5   V   sV     - 8\!'--fa8LD&o&vhvh778 \""r   c                *    t        |       }|r|d   S dS )Nr   )r   )r   r(   s     r   	base_namer8   c   s    T?D48##r   c                ^    t         j                  | xs d      }|r|j                  d      S dS )zNThe ``<repo>/.worktrees`` dir for a ``.../.worktrees/<task>`` path, else None.r   r0   N)_KANBAN_DIR_REr"   group)r   ms     r   kanban_worktree_dirr=   h   s+    TZR(A1771:$$r   c                    t        |       t        |      rt              t              kD  ryt        fdt        t                    D              S )zLTrue when ``target`` equals ``folder`` or is nested under it (segment-wise).Fc              3  4   K   | ]  }|   |   k(    y wNr   ).0ifts     r   	<genexpr>z!_is_path_under.<locals>.<genexpr>t   s     3qtqt|3s   )r*   lenallrange)foldertargetrC   rD   s     @@r   _is_path_underrK   n   sD    V$AV$AAQ3U3q6]333r   c                ^    t        j                  dd|       }t        j                  d||      S )N[/\\]+$r   z[^/\\]+$)r   sub)r   namestrippeds      r   _with_base_namerQ   w   s'    vvj"d+H66+tX..r   c           	     2    | t        |       xs | | |||||dS )N)repo_key
repo_label	repo_pathlane_key
lane_label	lane_pathis_main	is_kanban)r8   )r   rV   rW   rX   rY   rZ   s         r   
_placementr[      s0     	*7i 	 	r   c                p   t        |       }|syt        |       }|r:t        j                  ddt	        |d            }t        |t        |      d|dd      S t        j                  d|      }|r:t	        | |j                  d            }t        || |j                  d	      | dd      S t        | | || dd      S )
zDPath-only fallback when there is no git probe and no persisted root.NrM   r   kanbanFTz^(.+)-wt-(.+)$r0      )	r8   r=   r   rN   rQ   r[   r   r"   r;   )r   base
kanban_dirrU   r<   s        r   _place_by_heuristicra      s    T?D$T*JFF:r?:r+JK	)_Y%?:W\^bcc
"D)A#D!''!*5	)T1771:tUEJJdD$dE::r   c                   |r ||       nd }|r|j                  d      r|j                  d      r|d   }|d   }||k(  xs t        |j                  d            }|r6|xs dj                         xs t        }t	        |t        ||      ||dd      S t        |      }	|	rt	        |t        |      d|	dd      S t        |      xs |}
t	        |||
|dd      S |r\t        |       }	|	rt	        |t        |      d|	dd      S |xs dj                         xs t        }t	        |t        ||      ||dd      S t        |       S )Nr   worktree_rootrY   r   TFr]   )
getr!   r   DEFAULT_BRANCH_LABELr[   r   r=   r   r8   ra   )cwdr   resolvepersisted_rootinfor   rc   rY   br`   labels              r   _placerl      sS   "73<D%$((?*C%	_-9,ITXXi5H0I 2$$&>*>AiA)F9VZ\abb(7
i)CXz[`bfgg-(9M)]E=%QVWW (-
non.MxYcejlpqq\r  ":&:./.!*LaQ_aeglmms##r   c                    | j                  d      xs dj                         }|r"|r  ||      }|r|j                  d      r|d   S | j                  d      xs dj                         S )zCThe COMMON repo root a session belongs to (folds linked worktrees).rf   r   r   git_repo_root)rd   r   )sessionrg   rf   ri   s       r   _session_repo_rootrp      sb    ;;u#
*
*
,C
ws|DHH[)$$KK(.B5577r   c                .   t        | j                  d            xr | d   j                         t        v }t        | j                  d            }t	        d | j                  d      xs g D        d      }|rdnd	|rd	nd| | d   j                         fS )
NisMainrk   isKanbanc              3  2   K   | ]  }t        |        y wr@   _session_timerA   r   s     r   rE   z!_lane_sort_key.<locals>.<genexpr>   s     JM!$J   sessions        defaultr   r0   )r!   rd   lower_TRUNK_BRANCHESmax)r;   is_trunkrZ   activitys       r   _lane_sort_keyr      s     EIIh'(VU7^-A-A-C-VHUYYz*+IJeii
.C.IrJTWXH1A		g	 r   c                $    t        | t              S )N)key)sortedr   )groupss    r   _sort_lanesr      s    &n--r   c           	        i }| D ]&  }|j                  |d   g       j                  |       ( |j                         D ]!  }|D cg c]  }|j                  d      s| }}t	        |      dk  r4|D ci c]  }t        |      t        |d         dd  }}t        d |j                         D              }d}||k  si }	|D ]j  }|t        |         }
|rdj                  |
| d       nd	}t        |d         xs |d   }|r| d| n||d<   |	j                  |d   d
      dz   |	|d   <   l t        d |	j                         D              r|dz  }||k  r$ yc c}w c c}w )z>Grow colliding basenames into path-prefixed labels (in place).rk   r   r^   Nr7   c              3  2   K   | ]  }t        |        y wr@   )rF   )rA   ps     r   rE   z'_disambiguate_labels.<locals>.<genexpr>   s     91A9rx   r0   r,   r   r   c              3  &   K   | ]	  }|d k(    yw)r0   Nr   )rA   cs     r   rE   z'_disambiguate_labels.<locals>.<genexpr>   s     3a163s   )
setdefaultappendvaluesrd   rF   idr   r   r-   r8   rG   )itemsby_labelitembucketgpathedparents	max_depthdepthcountsr(   prefixr_   s                r   _disambiguate_labelsr      s   &(H <DM2.55d;< //# #5quuV}!55v;?=CD2a5)AfI.s33DD9(899	y %'F Cr!u~49$vw-0r 6+8qy39xq/t'
%+ZZ'
A%>%Bqz"C 36==?33QJE y 5 Es   E$E$6"E)c                d    t        | j                  d      xs | j                  d      xs d      S )Nlast_active
started_atr   )floatrd   )ro   s    r   rv   rv   
  s)    ]+Mw{{</HMANNr   c           	        i }| D ]  }|j                  d      xs dj                         }|s)t        ||j                  d      xs dj                         ||j                  d      xs dj                               }|s|t        |d         }|j                  |      }|+|d   |d   |d   |d   |d	   g d
|d   |d   |d   d}|||<   |d   d   j	                  |        i }	|j                         D ]  }|d   }
|
d   j                  t        d       t        |
d         }|sg |
d<   t        |d         }|	j                  |      }||d   |d   |d   g dd}||	|<   |d   j	                  |
       |dxx   |z  cc<    t        |	j                               }|D ]!  }t        |d         |d<   t        |d          # t        |       |S )zEBuild the ``repo -> lane -> sessions`` subtree for a set of sessions.rf   r   
git_branchrn   rV   rW   rX   rY   rZ   )r   rk   r   rr   rs   ry   rS   rT   rU   )r;   rS   rT   rU   r;   ry   Tr   reverser   r   rk   r   r   sessionCountr   r   )rd   r   rl   r5   r   r   sortrv   rF   r.   listr   r   )ry   rg   hydratelanesro   rf   	placementlane_identityentryreposr;   countrepo_identityrepo	repo_lists                  r   _build_reposr     s:   E 3{{5!'R..0[[&,"335[[)/R668	
	 !)J"78		-(= $J/&|4%k2'	2 )+ 6 " &j1'5&{3E $)E- gz"))'2?3B  E &gj=$?E*%& "E*!%
"34yy'<J'|,k* !D $(E- Xe$^%'&* U\\^$I -$T(^4XT(^,- #r   c           	     V   | D ch c]6  }|j                  d      |j                  d      fD ]  }|rt        |       8 }}}t        |       }|xs g D ]  }|j                  d      xs dj                         }|s)|r ||      nd}	|	xs i j                  d      xs t	        j
                  dd|      }
t        |
      }|r||v ru|j                  |
t        |
      xs |
|
g dd       |j                  |        t        |      t        |       k7  rt        |       |S c c}}w )	u  Ensure every declared project folder shows as a repo, even with 0 sessions.

    A brand-new project (or any project whose sessions haven't loaded yet) has an
    empty session-derived ``repos`` list. That breaks two things on the desktop:
    the entered-project view renders blank (it early-returns on no repos), and the
    optimistic live-session overlay has no lane to drop a freshly-created session
    into — so a new session in the project only appears after a full tree refresh.
    Seeding each folder as an empty repo fixes both: the overlay matches a new
    session's cwd under the folder root, and the drill-in renders a real (if
    empty) project body. Folders already covered by a session-derived repo (same
    git root) are left untouched.
    r   r   r   Nr   rM   r   r   )rd   r.   r   r   r   rN   r   r8   addrF   r   )r   foldersrg   r   r$   seenseededrI   rawri   r3   root_keys               r   _seed_folder_reposr   Q  s/   " hhtndhhv&67  	%D  %[F-R 
zz&!'R..0&ws|D
,Kz2s0KT?8t+TIdO,CtT]_qrst
 6{c%j V$M/s   ;D%c                       e Zd ZdZddZddZy)_FolderIndexu>  Maps a normalized folder path → (owning project, depth), so a session is
    matched to its project by walking its cwd's ancestors (O(path depth) dict
    lookups) instead of scanning every project × folder per session — the
    difference between O(sessions × projects) and O(sessions) at power-user scale.
    c                <   i | _         |D ]  }|j                  d      xs g D ]v  }t        |j                  d      xs d      }|s$dj                  |      }t	        |      }| j                   j                  |      }|	||d   kD  sf||f| j                   |<   x  y )Nr   r   r   r,   r0   )_by_pathrd   r*   r-   rF   )selfprojectsprojectrI   r(   r   r   existings           r   __init__z_FolderIndex.__init__  s    57 
	:G!++i06B 	:+FJJv,>,D"EhhtnD	==,,S1#ux{':*15)9DMM#&	:
	:r   c                    t        |xs d      }t        t        |      dd      D ]5  }| j                  j	                  dj                  |d|             }|s3|c S  y)zFOwning project for ``target`` by longest ancestor folder, + its depth.r   r   r7   r,   N)Nr7   )r*   rH   rF   r   rd   r-   )r   rJ   r(   endhits        r   r"   z_FolderIndex.match  s\    #FLb1TAr* 	C--##CHHT$3Z$89C
	 r   N)r   
list[dict]returnNone)rJ   strr   ztuple[Optional[dict], int])__name__
__module____qualname____doc__r   r"   r   r   r   r   r     s    :r   r   c                *    | j                  |      d   S )Nr   )r"   )indexrJ   s     r   _project_for_pathr     s    ;;vq!!r   c                    | j                  d      xs dj                         }|sy t        | |      }|r	||k7  r||gn|g}d }d}|D ]#  }|j                  |      \  }	}
|	s|
|kD  s |
}|	}% |S )Nrf   r   r7   )rd   r   rp   r"   )ro   r   rg   rf   r   
candidatesbestbest_lenrJ   r"   lengths              r   _project_for_sessionr     s    ;;u#
*
*
,C"7G4I%.93C#y!#JDH F+vVh&HD	
 Kr   F)coloriconis_autoc        
            | |||||	||||d
S )N)
r   rk   r   r   r   isAutor   
lastActiver   previewSessionsr   )
pidrk   r   r   session_countr   preview_sessionsr   r   r   s
             r   _project_noder     s,     %!+ r      )preview_limitr   is_junk_rootis_junk_cwdc               ~  &' | D cg c]  }|j                  d      r| }	}|xs d }
|xs d }t        |	      }i }g }|D ]G  }t        |||      }|r%|j                  |d   g       j	                  |       7|j	                  |       I g }d#fd}d$d}g }|	D ]  }|j                  |d   g       }|j                  d |D               t        t        |||      |j                  d      xs g |      }|j	                  t        |d   |j                  d	      xs |d   |j                  d
      |j                  d      |j                  d      |t        |       ||       ||      	              i 'd%'fd}|D ]  }t        ||      }|r |
|      s	 |||       #|j                  d      xs dj                         }|r ||      rQt        ||j                  d      xs dj                         ||j                  d      xs dj                               }|s ||d   |        t               }'j                         D ]  }|d   }|d   }t        |      &t        |||      }t!        &fd|D        d      } | =|j#                  &       |j                  d |D               |j	                  t        |t%        |      xs |||| d    ||       ||      d              |xs g D ]  }!|!j                  d      xs dj                         }"|"s*|r ||"      nd}#|#xs i j                  d      xs |"}t        |      }$|$|v s |
|      st'        ||      rs|j#                  |$       |!j                  d      xs t%        |      xs |}%|j	                  t        ||%|||%|g ddgt)        |!j                  d      xs d      t+        |!j                  d       xs d      g d             	 t-        |D cg c]  }|j                  d!      s| c}       ||d"S c c}w c c}w )&a  Build the authoritative project tree.

    ``projects`` are ``projects_db.Project.to_dict()`` shapes (non-archived).
    ``sessions`` are projected session-row dicts (must carry ``id``, ``cwd``,
    ``git_branch``, ``git_repo_root``, ``started_at``, ``last_active``).
    ``discovered_repos`` are ``{"root", "label", "sessions", "last_active"}``.
    ``is_junk_root`` flags git roots that must never become an AUTO project (the
    bare home dir, the HERMES_HOME subtree). ``is_junk_cwd`` is the narrower
    policy for non-git session folders: selected descendants may be intentional
    workspaces even when their parent tree contains Hermes state. User-created
    projects are honored regardless.

    Returns ``{"projects": [...], "scoped_session_ids": [...]}``. When
    ``hydrate`` is False (overview), lane ``sessions`` arrays are emptied but
    every count is preserved and each project carries up to ``preview_limit``
    ``previewSessions``. When True (drill-in), lanes carry full session rows.
    archivedc                     yNFr   )_roots    r   <lambda>zbuild_tree.<locals>.<lambda>      r   c                     yr   r   )_cwds    r   r   zbuild_tree.<locals>.<lambda>  r   r   r   c                @    dk  rg S t        | t        d      }|d  S )Nr   Tr   )r   rv   )project_sessionsorderedr   s     r   	_previewszbuild_tree.<locals>._previews   s,    AI)}dK~&&r   c                *    t        d | D        d      S )Nc              3  2   K   | ]  }t        |        y wr@   ru   rw   s     r   rE   z3build_tree.<locals>._last_active.<locals>.<genexpr>  s     ?M!$?rx   rz   r{   )r   )r   s    r   _last_activez build_tree.<locals>._last_active  s    ?.>?MMr   c              3  J   K   | ]  }|j                  d       s|d      ywr   Nrd   rw   s     r   rE   zbuild_tree.<locals>.<genexpr>  s     Dad!D'D   #
#r   rO   primary_pathr   r   )	r   rk   r   r   r   r   r   r   r   r3   c                t    t        |       }|sy j                  || g d      }|d   j                  |       y )N)r3   ry   ry   )r.   r   r   )r3   ro   r   r   by_auto_roots       r   	_add_autozbuild_tree.<locals>._add_auto&  s<    o((t.LMz!!'*r   rf   r   r   rn   rS   ry   c              3     K   | ]:  }t        |j                  d       xs |j                  d      xs d      k(  r| < yw)r   r   r   N)r.   rd   )rA   r   auto_keys     r   rE   zbuild_tree.<locals>.<genexpr>I  sA      TXXd^Etxx/?E2F(R s   A ANc              3  J   K   | ]  }|j                  d       s|d      ywr   r   rw   s     r   rE   zbuild_tree.<locals>.<genexpr>S  s     HaAEE$K!D'Hr   r   T)r   rk   r   r   r   r   r   r   r   rk   r   r   r   r   )r   scoped_session_ids)r   r   r   r   )r   r   r   r   )r3   r   ro   dictr   r   )rd   r   r   r   r   extendr   r   r   rF   rp   r   rl   setr   r.   nextr   r8   r   intr   r   )(r   ry   discovered_reposrg   r   r   r   r   r   active_projects_junk	_junk_cwdfolder_index
by_projectunownedro   owner
scoped_idsr   r   resultr   	psessionsr   r   r3   rf   r   r   r   	auto_rootauto_sessions	repo_noder   raw_rootri   r   rk   r  r   s(       `                                 @@r   
build_treer    sc   8 #+DQ!%%
2CqDOD10E3 2I0L(*JG $$WlGD!!%+r299'BNN7#$ J'N F # 
NN74="5	D9DD"GW5w{{97M7SQSU\
 	DMkk&):WT][[0kk'*[[(!)n(3!*9!5
	

0 %'L+  6!'73 ;$({{5!'R..0in[[&,"335[[)/R668	
	 i
+W5'6* UD%%' 
6N	z*Y']GW=!
 
	 H=HH	*7i'7(7!*=!9		
#
@ !&B 
HHV$*113$+wx 
,8T?tuT{.?d.S!<Yt_<"UDB`abc!$((:"6";!<!$((="9">Q?!#		

6 V?quuX!?@jAAa E\ @s   P5P5P:%P:)r   )r   r   r   r   r   r   )r   r   r   r   )r   r   r   z	list[str])r   r   r   r!   )r   r   r   r   )r1   r   r   r   )r   r   r   Optional[str])rI   r   rJ   r   r   r!   )r   r   rO   r   r   r   )r   r   rV   r   rW   r   rX   r   rY   r!   rZ   r!   r   r  )r   r   r   Optional[dict])
rf   r   r   r   rg   Optional[Resolve]rh   r   r   r  )ro   r  rg   r  r   r   )r;   r  r   tuple)r   r   r   r   )r   r   r   r   )ro   r  r   r   )ry   r   rg   r  r   r!   r   r   )r   r   r   r   rg   r  r   r   )r   r   rJ   r   r   r  )ro   r  r   r   rg   r  r   r  )r   r   rk   r   r   r  r   r   r   r  r   r   r   r   r   r   r   r   r   r!   r   r  r@   )r   r   ry   r   r	  r   rg   r  r   r  r   r!   r   Optional[Callable[[str], bool]]r   r  r   r  ))r   
__future__r   r   typingr   r   r   r   r  Resolvecompiler:   r~   re   r   r   r   r%   r*   r.   r5   r8   r=   rK   rQ   r[   ra   rl   rp   r   r   r   rv   r   r   r   r   r   r   r  r   r   r   <module>r"     s;  2 # 	 * * C5(4.(
)
 KL8 <
"LWX0

#$
%4/  	
   
(;&$B8.@O@F&& *&5F&&\ @"> 	  	
    !    
> "&	lB 4837lBlBlB !lB 	lB lB lB 2lB 1lB 
lBr   