1- import { type Accessor , createMemo , createSignal , Match , Show , Switch } from "solid-js"
2- import { useRouteData } from "@tui/context/route"
1+ import { type Accessor , createMemo , createSignal , For , Match , Show , Switch } from "solid-js"
2+ import { useRoute , useRouteData } from "@tui/context/route"
33import { useSync } from "@tui/context/sync"
44import { pipe , sumBy } from "remeda"
55import { useTheme } from "@tui/context/theme"
@@ -32,6 +32,7 @@ const ContextInfo = (props: { context: Accessor<string | undefined>; cost: Acces
3232
3333export function Header ( ) {
3434 const route = useRouteData ( "session" )
35+ const { navigate } = useRoute ( )
3536 const sync = useSync ( )
3637 const session = createMemo ( ( ) => sync . session . get ( route . sessionID ) ! )
3738 const messages = createMemo ( ( ) => sync . data . message [ route . sessionID ] ?? [ ] )
@@ -60,12 +61,113 @@ export function Header() {
6061 return result
6162 } )
6263
64+ // Build session path from root to current session
65+ const sessionPath = createMemo ( ( ) => {
66+ const path : Session [ ] = [ ]
67+ let current : Session | undefined = session ( )
68+ while ( current ) {
69+ path . unshift ( current )
70+ current = current . parentID ? sync . session . get ( current . parentID ) : undefined
71+ }
72+ return path
73+ } )
74+
75+ // Current depth (0 = root, 1 = first child, etc.)
76+ const depth = createMemo ( ( ) => sessionPath ( ) . length - 1 )
77+
78+ // Direct children of current session (for down navigation availability)
79+ const directChildren = createMemo ( ( ) => {
80+ const currentID = session ( ) ?. id
81+ if ( ! currentID ) return [ ]
82+ return sync . data . session . filter ( ( x ) => x . parentID === currentID )
83+ } )
84+
85+ // Siblings at current level (for left/right navigation availability)
86+ const siblings = createMemo ( ( ) => {
87+ const currentParentID = session ( ) ?. parentID
88+ if ( ! currentParentID ) return [ ]
89+ return sync . data . session . filter ( ( x ) => x . parentID === currentParentID )
90+ } )
91+
92+ // Navigation availability
93+ const canGoUp = createMemo ( ( ) => ! ! session ( ) ?. parentID )
94+ const canGoDown = createMemo ( ( ) => directChildren ( ) . length > 0 )
95+ const canCycleSiblings = createMemo ( ( ) => siblings ( ) . length > 1 )
96+
97+ // Get display name for a session
98+ const getSessionDisplayName = ( s : Session , isRoot : boolean ) => {
99+ if ( isRoot ) {
100+ // Root session: show the title
101+ return s . title || s . id . slice ( 0 , 8 )
102+ }
103+ // Child session: extract agent name from title like "Description (@agent-name subagent)"
104+ const match = s . title ?. match ( / \( @ ( [ ^ ) ] + ?) (?: \s + s u b a g e n t ) ? \) / )
105+ if ( match ) {
106+ // Return just the agent name without @ and "subagent"
107+ return match [ 1 ]
108+ }
109+ // Fallback to title or shortened ID
110+ return s . title || s . id . slice ( 0 , 8 )
111+ }
112+
113+ // Get UP navigation label based on depth
114+ const upLabel = createMemo ( ( ) => {
115+ const d = depth ( )
116+ if ( d <= 0 ) return "" // Root has no parent
117+ if ( d === 1 ) return "Parent" // Depth 1 → Root
118+ return `Child(L${ d - 1 } )` // Depth N → Child(L{N-1})
119+ } )
120+
121+ // Get DOWN navigation label based on depth
122+ const downLabel = createMemo ( ( ) => {
123+ const d = depth ( )
124+ return `Child(L${ d + 1 } )` // Depth N → Child(L{N+1})
125+ } )
126+
63127 const { theme } = useTheme ( )
64128 const keybind = useKeybind ( )
65129 const command = useCommandDialog ( )
66- const [ hover , setHover ] = createSignal < "parent" | "prev" | "next" | null > ( null )
67130 const dimensions = useTerminalDimensions ( )
68131 const narrow = createMemo ( ( ) => dimensions ( ) . width < 80 )
132+ const [ hover , setHover ] = createSignal < "parent" | "root" | "prev" | "next" | "down" | "breadcrumb" | null > ( null )
133+ const [ hoverBreadcrumbIdx , setHoverBreadcrumbIdx ] = createSignal < number | null > ( null )
134+
135+ // Calculate breadcrumb text for a set of segments
136+ const calcBreadcrumbLength = ( segments : Session [ ] , truncated : boolean ) => {
137+ let len = 0
138+ segments . forEach ( ( s , i ) => {
139+ len += getSessionDisplayName ( s , ! s . parentID ) . length
140+ if ( i < segments . length - 1 ) {
141+ len += truncated && i === 0 ? 9 : 3 // " > ... > " or " > "
142+ }
143+ } )
144+ return len
145+ }
146+
147+ // Dynamic breadcrumb truncation based on available width
148+ const breadcrumbSegments = createMemo ( ( ) => {
149+ const path = sessionPath ( )
150+ const availableWidth = dimensions ( ) . width - 40 // Reserve ~40 chars for right-side stats
151+
152+ // Try full path first
153+ const fullLength = calcBreadcrumbLength ( path , false )
154+ if ( fullLength <= availableWidth || path . length <= 2 ) {
155+ return { truncated : false , segments : path }
156+ }
157+
158+ // Truncate: show root + ... + last N segments that fit
159+ // Start with root + last segment, add more if space allows
160+ for ( let keepLast = path . length - 1 ; keepLast >= 1 ; keepLast -- ) {
161+ const segments = [ path [ 0 ] , ...path . slice ( - keepLast ) ]
162+ const len = calcBreadcrumbLength ( segments , true )
163+ if ( len <= availableWidth || keepLast === 1 ) {
164+ return { truncated : true , segments }
165+ }
166+ }
167+
168+ // Fallback: root + last segment
169+ return { truncated : true , segments : [ path [ 0 ] , path [ path . length - 1 ] ] }
170+ } )
69171
70172 return (
71173 < box flexShrink = { 0 } >
@@ -82,52 +184,125 @@ export function Header() {
82184 >
83185 < Switch >
84186 < Match when = { session ( ) ?. parentID } >
85- < box flexDirection = "column" gap = { 1 } >
86- < box flexDirection = { narrow ( ) ? "column" : "row" } justifyContent = "space-between" gap = { narrow ( ) ? 1 : 0 } >
87- < text fg = { theme . text } >
88- < b > Subagent session</ b >
89- </ text >
187+ { /* Subagent session: 3-row layout */ }
188+ < box flexDirection = "column" gap = { 0 } >
189+ { /* Row 1: Breadcrumb trail */ }
190+ < box flexDirection = "row" gap = { 0 } >
191+ < For each = { breadcrumbSegments ( ) . segments } >
192+ { ( segment , index ) => (
193+ < >
194+ < box
195+ onMouseOver = { ( ) => {
196+ setHover ( "breadcrumb" )
197+ setHoverBreadcrumbIdx ( index ( ) )
198+ } }
199+ onMouseOut = { ( ) => {
200+ setHover ( null )
201+ setHoverBreadcrumbIdx ( null )
202+ } }
203+ onMouseUp = { ( ) => {
204+ navigate ( { type : "session" , sessionID : segment . id } )
205+ } }
206+ backgroundColor = {
207+ hover ( ) === "breadcrumb" && hoverBreadcrumbIdx ( ) === index ( )
208+ ? theme . backgroundElement
209+ : theme . backgroundPanel
210+ }
211+ >
212+ < text fg = { index ( ) === breadcrumbSegments ( ) . segments . length - 1 ? theme . text : theme . textMuted } >
213+ < Show when = { index ( ) === breadcrumbSegments ( ) . segments . length - 1 } fallback = { getSessionDisplayName ( segment , ! segment . parentID ) } >
214+ < b > { getSessionDisplayName ( segment , ! segment . parentID ) } </ b >
215+ </ Show >
216+ </ text >
217+ </ box >
218+ < Show when = { index ( ) < breadcrumbSegments ( ) . segments . length - 1 } >
219+ { /* Show "... >" after root when truncated */ }
220+ < text fg = { theme . textMuted } >
221+ { index ( ) === 0 && breadcrumbSegments ( ) . truncated ? " > ... >" : " > " }
222+ </ text >
223+ </ Show >
224+ </ >
225+ ) }
226+ </ For >
227+ </ box >
228+
229+ { /* Row 2: Divider + stats */ }
230+ < box flexDirection = "row" gap = { 1 } >
231+ < box flexGrow = { 1 } >
232+ < text fg = { theme . border } > ────────────────────────────────────────</ text >
233+ </ box >
90234 < box flexDirection = "row" gap = { 1 } flexShrink = { 0 } >
91235 < ContextInfo context = { context } cost = { cost } />
92236 < text fg = { theme . textMuted } > v{ Installation . VERSION } </ text >
93237 </ box >
94238 </ box >
239+
240+ { /* Row 3: Navigation hints */ }
95241 < box flexDirection = "row" gap = { 2 } >
96- < box
97- onMouseOver = { ( ) => setHover ( "parent" ) }
98- onMouseOut = { ( ) => setHover ( null ) }
99- onMouseUp = { ( ) => command . trigger ( "session.parent" ) }
100- backgroundColor = { hover ( ) === "parent" ? theme . backgroundElement : theme . backgroundPanel }
101- >
102- < text fg = { theme . text } >
103- Parent < span style = { { fg : theme . textMuted } } > { keybind . print ( "session_parent" ) } </ span >
104- </ text >
105- </ box >
106- < box
107- onMouseOver = { ( ) => setHover ( "prev" ) }
108- onMouseOut = { ( ) => setHover ( null ) }
109- onMouseUp = { ( ) => command . trigger ( "session.child.previous" ) }
110- backgroundColor = { hover ( ) === "prev" ? theme . backgroundElement : theme . backgroundPanel }
111- >
112- < text fg = { theme . text } >
113- Prev < span style = { { fg : theme . textMuted } } > { keybind . print ( "session_child_cycle_reverse" ) } </ span >
114- </ text >
115- </ box >
116- < box
117- onMouseOver = { ( ) => setHover ( "next" ) }
118- onMouseOut = { ( ) => setHover ( null ) }
119- onMouseUp = { ( ) => command . trigger ( "session.child.next" ) }
120- backgroundColor = { hover ( ) === "next" ? theme . backgroundElement : theme . backgroundPanel }
121- >
122- < text fg = { theme . text } >
123- Next < span style = { { fg : theme . textMuted } } > { keybind . print ( "session_child_cycle" ) } </ span >
124- </ text >
125- </ box >
242+ < Show when = { canGoUp ( ) } >
243+ < box
244+ onMouseOver = { ( ) => setHover ( "parent" ) }
245+ onMouseOut = { ( ) => setHover ( null ) }
246+ onMouseUp = { ( ) => command . trigger ( "session.parent" ) }
247+ backgroundColor = { hover ( ) === "parent" ? theme . backgroundElement : theme . backgroundPanel }
248+ >
249+ < text fg = { theme . text } >
250+ { upLabel ( ) } < span style = { { fg : theme . textMuted } } > { keybind . print ( "session_parent" ) } </ span >
251+ </ text >
252+ </ box >
253+ </ Show >
254+ < Show when = { depth ( ) >= 2 } >
255+ < box
256+ onMouseOver = { ( ) => setHover ( "root" ) }
257+ onMouseOut = { ( ) => setHover ( null ) }
258+ onMouseUp = { ( ) => command . trigger ( "session.root" ) }
259+ backgroundColor = { hover ( ) === "root" ? theme . backgroundElement : theme . backgroundPanel }
260+ >
261+ < text fg = { theme . text } >
262+ Root < span style = { { fg : theme . textMuted } } > { keybind . print ( "session_root" ) } </ span >
263+ </ text >
264+ </ box >
265+ </ Show >
266+ < Show when = { canCycleSiblings ( ) } >
267+ < box
268+ onMouseOver = { ( ) => setHover ( "next" ) }
269+ onMouseOut = { ( ) => setHover ( null ) }
270+ onMouseUp = { ( ) => command . trigger ( "session.child.next" ) }
271+ backgroundColor = { hover ( ) === "next" ? theme . backgroundElement : theme . backgroundPanel }
272+ >
273+ < text fg = { theme . text } >
274+ Next < span style = { { fg : theme . textMuted } } > { keybind . print ( "session_child_cycle" ) } </ span >
275+ </ text >
276+ </ box >
277+ < box
278+ onMouseOver = { ( ) => setHover ( "prev" ) }
279+ onMouseOut = { ( ) => setHover ( null ) }
280+ onMouseUp = { ( ) => command . trigger ( "session.child.previous" ) }
281+ backgroundColor = { hover ( ) === "prev" ? theme . backgroundElement : theme . backgroundPanel }
282+ >
283+ < text fg = { theme . text } >
284+ Prev < span style = { { fg : theme . textMuted } } > { keybind . print ( "session_child_cycle_reverse" ) } </ span >
285+ </ text >
286+ </ box >
287+ </ Show >
288+ < Show when = { canGoDown ( ) } >
289+ < box
290+ onMouseOver = { ( ) => setHover ( "down" ) }
291+ onMouseOut = { ( ) => setHover ( null ) }
292+ onMouseUp = { ( ) => command . trigger ( "session.child.down" ) }
293+ backgroundColor = { hover ( ) === "down" ? theme . backgroundElement : theme . backgroundPanel }
294+ >
295+ < text fg = { theme . text } >
296+ { downLabel ( ) } < span style = { { fg : theme . textMuted } } > { keybind . print ( "session_child_down" ) } </ span >
297+ </ text >
298+ </ box >
299+ </ Show >
126300 </ box >
127301 </ box >
128302 </ Match >
129303 < Match when = { true } >
130- < box flexDirection = { narrow ( ) ? "column" : "row" } justifyContent = "space-between" gap = { 1 } >
304+ { /* Root session: responsive layout from upstream */ }
305+ < box flexDirection = { narrow ( ) ? "column" : "row" } justifyContent = "space-between" gap = { narrow ( ) ? 1 : 0 } >
131306 < Title session = { session } />
132307 < box flexDirection = "row" gap = { 1 } flexShrink = { 0 } >
133308 < ContextInfo context = { context } cost = { cost } />
0 commit comments