hiiro 0.1.30 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- module Task
3
+ module OldTask
4
4
  def self.load(hiiro)
5
5
  hiiro.load_plugin(Tmux)
6
6
  attach_methods(hiiro)
@@ -58,6 +58,8 @@ module Task
58
58
  tasks.new_subtask(subtask_name)
59
59
  in ['subtask', 'switch', subtask_name]
60
60
  tasks.switch_subtask(subtask_name)
61
+ in ['subtask', 'switch']
62
+ tasks.switch_subtask
61
63
  in [subcmd, *sargs]
62
64
  match = runner_map.keys.find { |full_subcmd| full_subcmd.to_s.start_with?(subcmd) }
63
65
 
@@ -77,7 +79,7 @@ module Task
77
79
  def self.attach_methods(hiiro)
78
80
  hiiro.instance_eval do
79
81
  def task_manager
80
- @task_manager ||= Task::TaskManager.new(self)
82
+ @task_manager ||= OldTask::TaskManager.new(self)
81
83
  end
82
84
  end
83
85
  end
@@ -119,16 +121,50 @@ module Task
119
121
  current = current_task
120
122
  active, available = trees.partition { |tree_name| task_for_tree(tree_name) }
121
123
 
124
+ # Group active trees by parent task
125
+ groups = {}
122
126
  active.each do |tree_name|
123
127
  task = task_for_tree(tree_name)
124
- marker = (current && current[:tree] == tree_name) ? "*" : " "
125
- puts format("%s %-20s => %s", marker, tree_name, task)
128
+ parent = task.include?('/') ? task.split('/').first : task
129
+ groups[parent] ||= []
130
+ groups[parent] << { tree: tree_name, task: task }
126
131
  end
127
132
 
128
- puts if active.any? && available.any?
133
+ first_group = true
134
+ groups.each do |parent, entries|
135
+ puts unless first_group
136
+ first_group = false
137
+
138
+ # Sort so the main entry (parent itself or parent/main) comes first
139
+ entries.sort_by! { |e| e[:task] == parent || e[:task].end_with?('/main') ? 0 : 1 }
140
+
141
+ entries.each_with_index do |entry, i|
142
+ tree_name = entry[:tree]
143
+ task = entry[:task]
144
+ marker = (current && current[:tree] == tree_name) ? "*" : " "
145
+ branch = worktree_branch(tree_name)
146
+ branch_str = branch ? " [#{branch}]" : ""
147
+
148
+ if i == 0
149
+ # Parent task line
150
+ display_name = parent
151
+ puts format("%s %s%s", marker, display_name, branch_str)
152
+ else
153
+ # Subtask line: align /child_name under the parent name
154
+ child_name = task.include?('/') ? task.split('/', 2).last : task
155
+ padding = " " * parent.length
156
+ puts format("%s %s/%s%s", marker, padding, child_name, branch_str)
157
+ end
158
+ end
159
+ end
129
160
 
130
- available.each do |tree_name|
131
- puts format(" %-20s (available)", tree_name)
161
+ if available.any?
162
+ puts
163
+ available.each do |tree_name|
164
+ branch = worktree_branch(tree_name)
165
+ branch_str = branch ? " [#{branch}]" : ""
166
+ puts format(" %-20s (available)%s", tree_name, branch_str)
167
+ end
132
168
  end
133
169
  end
134
170
 
@@ -395,6 +431,8 @@ module Task
395
431
  list_subtasks
396
432
  in ['new', subtask_name]
397
433
  new_subtask(subtask_name)
434
+ in ['switch']
435
+ switch_subtask
398
436
  in ['switch', subtask_name]
399
437
  switch_subtask(subtask_name)
400
438
  in ['app', app_name]
@@ -437,22 +475,27 @@ module Task
437
475
  end
438
476
  subtasks = subtasks_for_task(parent_task)
439
477
 
440
- if subtasks.empty?
441
- puts "No subtasks for task '#{parent_task}'."
442
- puts
443
- puts "Create one with 'h subtask new SUBTASK_NAME'"
444
- return
445
- end
446
-
447
478
  puts "Subtasks for '#{parent_task}':"
448
479
  puts
480
+
481
+ # Always show the parent (main) task first
482
+ parent_tree = tree_for_task(parent_task)
483
+ current_tree = current[:tree]
484
+ parent_marker = (parent_tree && parent_tree == current_tree) ? "*" : " "
485
+ parent_branch = worktree_branch(parent_tree) if parent_tree
486
+ branch_info = parent_branch ? " branch: #{parent_branch}" : ""
487
+ puts format("%s %-25s tree: %-15s%s", parent_marker, "(main)", parent_tree || '(none)', branch_info)
488
+
449
489
  subtasks.each do |subtask|
450
- marker = subtask['active'] ? "*" : " "
451
- puts format("%s %-25s tree: %-15s created: %s",
490
+ marker = (subtask['worktree'] == current_tree) ? "*" : " "
491
+ st_branch = worktree_branch(subtask['worktree']) if subtask['worktree']
492
+ st_branch_info = st_branch ? " branch: #{st_branch}" : ""
493
+ puts format("%s %-25s tree: %-15s created: %s%s",
452
494
  marker,
453
495
  subtask['name'],
454
496
  subtask['worktree'] || '(none)',
455
- subtask['created_at']&.split('T')&.first || '?'
497
+ subtask['created_at']&.split('T')&.first || '?',
498
+ st_branch_info
456
499
  )
457
500
  end
458
501
  end
@@ -519,7 +562,7 @@ module Task
519
562
  true
520
563
  end
521
564
 
522
- def switch_subtask(subtask_name)
565
+ def switch_subtask(subtask_name = nil)
523
566
  current = current_task
524
567
  unless current
525
568
  puts "ERROR: Not currently in a task session"
@@ -533,6 +576,41 @@ module Task
533
576
  parent_task = parent_task.split('/').first
534
577
  end
535
578
 
579
+ # If no subtask name, use interactive selection
580
+ if subtask_name.nil? || subtask_name.empty?
581
+ subtask_name = select_subtask_interactive(parent_task)
582
+ return false unless subtask_name
583
+ end
584
+
585
+ # Switch to parent task if "main" or "parent"
586
+ if subtask_name == 'main' || subtask_name == 'parent'
587
+ session_name = session_name_for(parent_task)
588
+ tree_name = tree_for_task(parent_task)
589
+
590
+ unless tree_name
591
+ puts "ERROR: Parent task '#{parent_task}' has no worktree"
592
+ return false
593
+ end
594
+
595
+ session_exists = system('tmux', 'has-session', '-t', session_name, err: File::NULL)
596
+
597
+ if session_exists
598
+ hiiro.start_tmux_session(session_name)
599
+ else
600
+ path = tree_path(tree_name)
601
+ if Dir.exist?(path)
602
+ Dir.chdir(path)
603
+ hiiro.start_tmux_session(session_name)
604
+ else
605
+ puts "ERROR: Worktree path '#{path}' does not exist"
606
+ return false
607
+ end
608
+ end
609
+
610
+ puts "Switched to parent task '#{parent_task}'"
611
+ return true
612
+ end
613
+
536
614
  subtask = find_subtask(parent_task, subtask_name)
537
615
  unless subtask
538
616
  puts "Subtask '#{subtask_name}' not found for task '#{parent_task}'"
@@ -565,6 +643,36 @@ module Task
565
643
  true
566
644
  end
567
645
 
646
+ # Interactive subtask selection using sk
647
+ def select_subtask_interactive(parent_task)
648
+ subtasks = subtasks_for_task(parent_task)
649
+ parent_tree = tree_for_task(parent_task)
650
+
651
+ # Build selection lines: include parent (main) and all subtasks
652
+ lines = []
653
+ lines << "(main)" if parent_tree
654
+ subtasks.each do |subtask|
655
+ next unless subtask['active']
656
+ lines << subtask['name']
657
+ end
658
+
659
+ if lines.empty?
660
+ puts "No subtasks to switch to."
661
+ return nil
662
+ end
663
+
664
+ require 'open3'
665
+ selected, status = Open3.capture2('sk', stdin_data: lines.join("\n"))
666
+
667
+ if status.success? && !selected.strip.empty?
668
+ choice = selected.strip
669
+ return 'main' if choice == '(main)'
670
+ return choice
671
+ end
672
+
673
+ nil
674
+ end
675
+
568
676
  # Show status for the current subtask
569
677
  def subtask_status
570
678
  current = current_task
@@ -774,11 +882,11 @@ module Task
774
882
  worktree_info.keys.sort
775
883
  end
776
884
 
777
- # Parse git worktree list output into { name => path } hash
778
- def worktree_info
779
- @worktree_info ||= begin
885
+ # Parse git worktree list output into { name => { path:, branch: } } hash
886
+ def worktree_details
887
+ @worktree_details ||= begin
780
888
  output = `git -C #{main_repo_path} worktree list --porcelain 2>/dev/null`
781
- info = {}
889
+ details = {}
782
890
  current_path = nil
783
891
 
784
892
  work_dir = File.join(Dir.home, 'work')
@@ -792,6 +900,12 @@ module Task
792
900
  # Skip bare repo
793
901
  current_path = nil
794
902
  elsif line.start_with?('branch ') || line == 'detached'
903
+ branch = if line.start_with?('branch ')
904
+ line.sub('branch refs/heads/', '')
905
+ else
906
+ '(detached)'
907
+ end
908
+
795
909
  # Capture worktree (both named branches and detached HEAD)
796
910
  if current_path && current_path != main_repo_path
797
911
  # Use relative path from ~/work/ to support nested worktrees
@@ -801,17 +915,28 @@ module Task
801
915
  else
802
916
  File.basename(current_path)
803
917
  end
804
- info[name] = current_path
918
+ details[name] = { path: current_path, branch: branch }
805
919
  end
806
920
  current_path = nil
807
921
  end
808
922
  end
809
923
 
810
- info
924
+ details
811
925
  end
812
926
  end
813
927
 
928
+ # Backward-compatible { name => path } hash
929
+ def worktree_info
930
+ @worktree_info ||= worktree_details.transform_values { |v| v[:path] }
931
+ end
932
+
933
+ # Get branch name for a worktree
934
+ def worktree_branch(tree_name)
935
+ worktree_details.dig(tree_name, :branch)
936
+ end
937
+
814
938
  def clear_worktree_cache
939
+ @worktree_details = nil
815
940
  @worktree_info = nil
816
941
  end
817
942