@@ -109,6 +109,16 @@ pub enum IssuesFocus {
109109 Details ,
110110}
111111
112+ /// which section is active in the detail pane.
113+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Default ) ]
114+ pub enum DetailSection {
115+ /// "Blocked by:" (dependencies)
116+ #[ default]
117+ Deps ,
118+ /// "Blocks:" (dependents)
119+ Dependents ,
120+ }
121+
112122/// input mode for creating/editing issues.
113123#[ derive( Debug , Clone , PartialEq , Eq ) ]
114124pub enum InputMode {
@@ -164,6 +174,10 @@ pub struct App {
164174 pub config : Config ,
165175 /// selected dependency index in detail pane
166176 pub detail_dep_selected : Option < usize > ,
177+ /// which section is active in the detail pane (deps or dependents)
178+ pub detail_section : DetailSection ,
179+ /// selected dependent index in detail pane
180+ pub detail_dependent_selected : Option < usize > ,
167181 /// filter query
168182 pub filter_query : String ,
169183 /// status filter (empty means show all)
@@ -249,6 +263,8 @@ impl App {
249263 input_mode : InputMode :: Normal ,
250264 config,
251265 detail_dep_selected : None ,
266+ detail_section : DetailSection :: default ( ) ,
267+ detail_dependent_selected : None ,
252268 filter_query : String :: new ( ) ,
253269 status_filter : HashSet :: new ( ) ,
254270 editor_file : None ,
@@ -1141,6 +1157,58 @@ impl App {
11411157 }
11421158 }
11431159
1160+ /// select dependent by index (0-based).
1161+ pub fn select_dependent_by_index ( & mut self , idx : usize ) {
1162+ let Some ( issue) = self . selected_issue ( ) else {
1163+ return ;
1164+ } ;
1165+ let dependents = crate :: graph:: get_dependents ( issue. id ( ) , & self . issues ) ;
1166+ if idx < dependents. len ( ) {
1167+ self . detail_dependent_selected = Some ( idx) ;
1168+ self . message = None ;
1169+ }
1170+ }
1171+
1172+ /// open the selected dependent in the list view.
1173+ pub fn open_selected_dependent ( & mut self ) {
1174+ let dependent_id = {
1175+ let Some ( issue) = self . selected_issue ( ) else {
1176+ self . message = Some ( "no issue selected" . to_string ( ) ) ;
1177+ return ;
1178+ } ;
1179+ let dependents = crate :: graph:: get_dependents ( issue. id ( ) , & self . issues ) ;
1180+ if dependents. is_empty ( ) {
1181+ self . message = Some ( "no dependents" . to_string ( ) ) ;
1182+ return ;
1183+ }
1184+ let Some ( dep_idx) = self . detail_dependent_selected else {
1185+ self . message = Some ( "no dependent selected" . to_string ( ) ) ;
1186+ return ;
1187+ } ;
1188+
1189+ // sort dependents the same way as rendering: open/doing first
1190+ let mut sorted = dependents;
1191+ sorted. sort_by_key ( |id| {
1192+ self . issues
1193+ . get ( id)
1194+ . map ( |iss| matches ! ( iss. status( ) , Status :: Done | Status :: Skip ) as u8 )
1195+ . unwrap_or ( 0 )
1196+ } ) ;
1197+
1198+ let Some ( id) = sorted. get ( dep_idx) else {
1199+ self . message = Some ( "dependent not found" . to_string ( ) ) ;
1200+ return ;
1201+ } ;
1202+ id. to_string ( )
1203+ } ;
1204+
1205+ if self . select_issue_by_id ( & dependent_id) {
1206+ self . message = None ;
1207+ } else {
1208+ self . message = Some ( "dependent issue missing" . to_string ( ) ) ;
1209+ }
1210+ }
1211+
11441212 // start/done keybindings removed - too easy to trigger accidentally
11451213 // use CLI commands (brd start, brd done) instead
11461214
@@ -1288,6 +1356,27 @@ impl App {
12881356 } else {
12891357 self . detail_dep_selected = Some ( 0 ) ;
12901358 }
1359+
1360+ // reset dependent selection
1361+ let dependents_len = self
1362+ . selected_issue ( )
1363+ . map ( |issue| crate :: graph:: get_dependents ( issue. id ( ) , & self . issues ) . len ( ) )
1364+ . unwrap_or ( 0 ) ;
1365+ if dependents_len == 0 {
1366+ self . detail_dependent_selected = None ;
1367+ } else {
1368+ self . detail_dependent_selected = Some ( 0 ) ;
1369+ }
1370+
1371+ // reset section: prefer deps if available, otherwise dependents
1372+ if deps_len > 0 {
1373+ self . detail_section = DetailSection :: Deps ;
1374+ } else if dependents_len > 0 {
1375+ self . detail_section = DetailSection :: Dependents ;
1376+ } else {
1377+ self . detail_section = DetailSection :: Deps ;
1378+ }
1379+
12911380 // also reset detail scroll when changing issue
12921381 self . detail_scroll = 0 ;
12931382 }
@@ -1307,6 +1396,31 @@ impl App {
13071396 } else {
13081397 self . detail_dep_selected = Some ( 0 ) ;
13091398 }
1399+
1400+ // clamp dependent selection
1401+ let dependents_len = self
1402+ . selected_issue ( )
1403+ . map ( |issue| crate :: graph:: get_dependents ( issue. id ( ) , & self . issues ) . len ( ) )
1404+ . unwrap_or ( 0 ) ;
1405+ if dependents_len == 0 {
1406+ self . detail_dependent_selected = None ;
1407+ } else if let Some ( idx) = self . detail_dependent_selected {
1408+ if idx >= dependents_len {
1409+ self . detail_dependent_selected = Some ( dependents_len - 1 ) ;
1410+ }
1411+ } else {
1412+ self . detail_dependent_selected = Some ( 0 ) ;
1413+ }
1414+
1415+ // if the active section has no items, switch to the other section
1416+ if self . detail_section == DetailSection :: Deps && deps_len == 0 && dependents_len > 0 {
1417+ self . detail_section = DetailSection :: Dependents ;
1418+ } else if self . detail_section == DetailSection :: Dependents
1419+ && dependents_len == 0
1420+ && deps_len > 0
1421+ {
1422+ self . detail_section = DetailSection :: Deps ;
1423+ }
13101424 }
13111425
13121426 fn select_issue_by_id ( & mut self , issue_id : & str ) -> bool {
@@ -1988,6 +2102,38 @@ mod tests {
19882102 assert_eq ! ( app. detail_dep_selected, Some ( 2 ) ) ;
19892103 }
19902104
2105+ #[ test]
2106+ fn test_reload_preserves_dependent_selection ( ) {
2107+ let env = TestEnv :: new ( ) ;
2108+ env. add_issue ( "brd-main" , "main issue" , Priority :: P1 , Status :: Open ) ;
2109+ env. add_issue_with_deps (
2110+ "brd-dep1" ,
2111+ "dependent one" ,
2112+ Priority :: P2 ,
2113+ Status :: Open ,
2114+ vec ! [ "brd-main" ] ,
2115+ ) ;
2116+ env. add_issue_with_deps (
2117+ "brd-dep2" ,
2118+ "dependent two" ,
2119+ Priority :: P3 ,
2120+ Status :: Open ,
2121+ vec ! [ "brd-main" ] ,
2122+ ) ;
2123+
2124+ let mut app = env. app ( ) ;
2125+ assert_eq ! ( app. selected_issue_id( ) , Some ( "brd-main" ) ) ;
2126+ assert_eq ! ( app. detail_dependent_selected, Some ( 0 ) ) ;
2127+
2128+ // select dependent index 1
2129+ app. select_dependent_by_index ( 1 ) ;
2130+ assert_eq ! ( app. detail_dependent_selected, Some ( 1 ) ) ;
2131+
2132+ // reload issues - should preserve dependent selection
2133+ app. reload_issues ( & env. paths ) . expect ( "reload failed" ) ;
2134+ assert_eq ! ( app. detail_dependent_selected, Some ( 1 ) ) ;
2135+ }
2136+
19912137 #[ test]
19922138 fn test_reload_clamps_dep_selection_when_deps_removed ( ) {
19932139 let env = TestEnv :: new ( ) ;
@@ -2018,4 +2164,63 @@ mod tests {
20182164 app. reload_issues ( & env. paths ) . expect ( "reload failed" ) ;
20192165 assert_eq ! ( app. detail_dep_selected, Some ( 0 ) ) ;
20202166 }
2167+
2168+ #[ test]
2169+ fn test_reload_clamps_dependent_selection_when_dependents_removed ( ) {
2170+ let env = TestEnv :: new ( ) ;
2171+ env. add_issue ( "brd-main" , "main issue" , Priority :: P1 , Status :: Open ) ;
2172+ env. add_issue_with_deps (
2173+ "brd-dep1" ,
2174+ "dependent one" ,
2175+ Priority :: P2 ,
2176+ Status :: Open ,
2177+ vec ! [ "brd-main" ] ,
2178+ ) ;
2179+ env. add_issue_with_deps (
2180+ "brd-dep2" ,
2181+ "dependent two" ,
2182+ Priority :: P3 ,
2183+ Status :: Open ,
2184+ vec ! [ "brd-main" ] ,
2185+ ) ;
2186+
2187+ let mut app = env. app ( ) ;
2188+ assert_eq ! ( app. selected_issue_id( ) , Some ( "brd-main" ) ) ;
2189+
2190+ // select dependent index 1
2191+ app. select_dependent_by_index ( 1 ) ;
2192+ assert_eq ! ( app. detail_dependent_selected, Some ( 1 ) ) ;
2193+
2194+ // remove one dependent by removing its dep on brd-main
2195+ let issue_path = env. paths . issues_dir ( & env. config ) . join ( "brd-dep2.md" ) ;
2196+ let mut issue = app. issues . get ( "brd-dep2" ) . unwrap ( ) . clone ( ) ;
2197+ issue. frontmatter . deps = vec ! [ ] ;
2198+ issue. save ( & issue_path) . expect ( "failed to save" ) ;
2199+
2200+ // reload - should clamp to max valid index (0)
2201+ app. reload_issues ( & env. paths ) . expect ( "reload failed" ) ;
2202+ assert_eq ! ( app. detail_dependent_selected, Some ( 0 ) ) ;
2203+ }
2204+
2205+ #[ test]
2206+ fn test_reset_dep_selection_sets_section_to_dependents_when_no_deps ( ) {
2207+ let env = TestEnv :: new ( ) ;
2208+ // brd-main has no deps but is depended on by brd-dep1
2209+ env. add_issue ( "brd-main" , "main issue" , Priority :: P1 , Status :: Open ) ;
2210+ env. add_issue_with_deps (
2211+ "brd-dep1" ,
2212+ "dependent" ,
2213+ Priority :: P2 ,
2214+ Status :: Open ,
2215+ vec ! [ "brd-main" ] ,
2216+ ) ;
2217+
2218+ let app = env. app ( ) ;
2219+ assert_eq ! ( app. selected_issue_id( ) , Some ( "brd-main" ) ) ;
2220+
2221+ // no deps, so section should default to Dependents
2222+ assert_eq ! ( app. detail_section, DetailSection :: Dependents ) ;
2223+ assert_eq ! ( app. detail_dep_selected, None ) ;
2224+ assert_eq ! ( app. detail_dependent_selected, Some ( 0 ) ) ;
2225+ }
20212226}
0 commit comments