hiiro 0.1.29 → 0.1.31

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9bb0956fd77df5e3e941a772cedc2fb51376f7be1adb7ce40b217d73a300d058
4
- data.tar.gz: 9436e79dae25ae352dcadfbea5e3dcaf1e3be6d435fc4ea5823b71e7425fc739
3
+ metadata.gz: f9acf06495590129ec1870e030280f89addc30177e768c360aa8e6891d69ece7
4
+ data.tar.gz: afdc44652ae0ea1efbc107f8bd4b972c88477f07cf247665a66104859f49a0c2
5
5
  SHA512:
6
- metadata.gz: 3b84f6e8bb918e7029af357a84bf4debe20a2467e9be85d77ea5e931e1b8ac64097c45b9d29b88e8db6514b7303798ef051adcb88144ff7e534c6ab0b2f4bbab
7
- data.tar.gz: d901ce7695933da9452d7a08cd95cdf28b706af8e46c34eb68dbc0d4500ce7a8b4d0a479fe5b58e8201afe55131b6aa6249d682ef166632a819e11a878440b5a
6
+ metadata.gz: 74d7b72bcb20c9cfdde0ef7c63d02d270f6f95fd05510fb5ff25f0543c4830f15418718f693fce532c8388f9c9a80851a9d13969646d75b187119f08d3d9cd64
7
+ data.tar.gz: 5f00c18b9bf9c47f8063a37dd6984bcbf97d7bdc4d06f06d918e1aa93aa48c316a14fd76301c56e029eafab2acacbc163f3e50c040b6d85f64217611554a56ae
data/bin/h-subtask CHANGED
@@ -16,7 +16,7 @@ hiiro.add_subcmd(:edit) { system(ENV['EDITOR'] || 'nvim', __FILE__) }
16
16
  hiiro.add_subcmd(:list) { tasks.list_subtasks }
17
17
  hiiro.add_subcmd(:ls) { tasks.list_subtasks }
18
18
  hiiro.add_subcmd(:new) { |subtask_name| tasks.new_subtask(subtask_name) }
19
- hiiro.add_subcmd(:switch) { |subtask_name| tasks.switch_subtask(subtask_name) }
19
+ hiiro.add_subcmd(:switch) { |subtask_name=nil| tasks.switch_subtask(subtask_name) }
20
20
  hiiro.add_subcmd(:app) { |app_name| tasks.subtask_open_app(app_name) }
21
21
  hiiro.add_subcmd(:path) { |app_name=nil| tasks.subtask_app_path(app_name) }
22
22
  hiiro.add_subcmd(:cd) { |*args| tasks.subtask_cd_app(*args) }
data/bin/h-task CHANGED
@@ -44,23 +44,55 @@ class TaskManager
44
44
  active, available = trees.partition { |tree_name| task_for_tree(tree_name) }
45
45
  sessions = tmux_sessions
46
46
 
47
+ # Group active trees by parent task
48
+ groups = {}
47
49
  active.each do |tree_name|
48
50
  task = task_for_tree(tree_name)
49
- marker = (current && current[:tree] == tree_name) ? "*" : " "
50
-
51
- # Check if there's a tmux session for this task
52
- session_name = session_name_for(task)
53
- has_session = sessions.include?(session_name)
54
- session_marker = has_session ? "+" : " "
55
- session_info = has_session ? " (#{session_name})" : ""
56
-
57
- puts format("%s%s %-20s => %s%s", marker, session_marker, tree_name, task, session_info)
51
+ parent = task.include?('/') ? task.split('/').first : task
52
+ groups[parent] ||= []
53
+ groups[parent] << { tree: tree_name, task: task }
54
+ end
55
+
56
+ first_group = true
57
+ groups.each do |parent, entries|
58
+ puts unless first_group
59
+ first_group = false
60
+
61
+ # Sort so the main entry (parent itself or parent/main) comes first
62
+ entries.sort_by! { |e| e[:task] == parent || e[:task].end_with?('/main') ? 0 : 1 }
63
+
64
+ entries.each_with_index do |entry, i|
65
+ tree_name = entry[:tree]
66
+ task = entry[:task]
67
+ marker = (current && current[:tree] == tree_name) ? "*" : " "
68
+ branch = worktree_branch(tree_name)
69
+ branch_str = branch ? " [#{branch}]" : ""
70
+
71
+ # Check if there's a tmux session for this task
72
+ session_name = session_name_for(task)
73
+ has_session = sessions.include?(session_name)
74
+ session_marker = has_session ? "+" : " "
75
+
76
+ if i == 0
77
+ # Parent task line
78
+ display_name = parent
79
+ puts format("%s%s %s%s", marker, session_marker, display_name, branch_str)
80
+ else
81
+ # Subtask line: align /child_name under the parent name
82
+ child_name = task.include?('/') ? task.split('/', 2).last : task
83
+ padding = " " * parent.length
84
+ puts format("%s%s %s/%s%s", marker, session_marker, padding, child_name, branch_str)
85
+ end
86
+ end
58
87
  end
59
88
 
60
- puts if active.any? && available.any?
61
-
62
- available.each do |tree_name|
63
- puts format(" %-20s (available)", tree_name)
89
+ if available.any?
90
+ puts
91
+ available.each do |tree_name|
92
+ branch = worktree_branch(tree_name)
93
+ branch_str = branch ? " [#{branch}]" : ""
94
+ puts format(" %-20s (available)%s", tree_name, branch_str)
95
+ end
64
96
  end
65
97
 
66
98
  # List tmux sessions without associated tasks
@@ -415,11 +447,11 @@ class TaskManager
415
447
  worktree_info.keys.sort
416
448
  end
417
449
 
418
- # Parse git worktree list output into { name => path } hash
419
- def worktree_info
420
- @worktree_info ||= begin
450
+ # Parse git worktree list output into { name => { path:, branch: } } hash
451
+ def worktree_details
452
+ @worktree_details ||= begin
421
453
  output = `git -C #{main_repo_path} worktree list --porcelain 2>/dev/null`
422
- info = {}
454
+ details = {}
423
455
  current_path = nil
424
456
 
425
457
  work_dir = File.join(Dir.home, 'work')
@@ -433,6 +465,12 @@ class TaskManager
433
465
  # Skip bare repo
434
466
  current_path = nil
435
467
  elsif line.start_with?('branch ') || line == 'detached'
468
+ branch = if line.start_with?('branch ')
469
+ line.sub('branch refs/heads/', '')
470
+ else
471
+ '(detached)'
472
+ end
473
+
436
474
  # Capture worktree (both named branches and detached HEAD)
437
475
  if current_path && current_path != main_repo_path
438
476
  # Use relative path from ~/work/ to support nested worktrees
@@ -442,17 +480,28 @@ class TaskManager
442
480
  else
443
481
  File.basename(current_path)
444
482
  end
445
- info[name] = current_path
483
+ details[name] = { path: current_path, branch: branch }
446
484
  end
447
485
  current_path = nil
448
486
  end
449
487
  end
450
488
 
451
- info
489
+ details
452
490
  end
453
491
  end
454
492
 
493
+ # Backward-compatible { name => path } hash
494
+ def worktree_info
495
+ @worktree_info ||= worktree_details.transform_values { |v| v[:path] }
496
+ end
497
+
498
+ # Get branch name for a worktree
499
+ def worktree_branch(tree_name)
500
+ worktree_details.dig(tree_name, :branch)
501
+ end
502
+
455
503
  def clear_worktree_cache
504
+ @worktree_details = nil
456
505
  @worktree_info = nil
457
506
  end
458
507
 
@@ -701,8 +750,8 @@ class TaskManager
701
750
 
702
751
  # Migrate flat worktrees (~/work/task) to nested (~/work/task/main)
703
752
  def migrate_worktrees
704
- # Find flat worktrees that need migration (not already nested, not reserved)
705
- flat_trees = trees.reject { |name| name.include?('/') || RESERVED_WORKTREES.key?(name) }
753
+ # Find flat worktrees that need migration (not already nested)
754
+ flat_trees = trees.reject { |name| name.include?('/') }
706
755
 
707
756
  if flat_trees.empty?
708
757
  puts "No flat worktrees to migrate."
@@ -751,7 +800,7 @@ class TaskManager
751
800
  data = load_assignments
752
801
  data.delete(name)
753
802
  data[new_tree_name] = task
754
- save_assignments(RESERVED_WORKTREES.merge(data))
803
+ save_assignments(data)
755
804
  end
756
805
 
757
806
  # Step 4: Update task metadata
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.29"
2
+ VERSION = "0.1.31"
3
3
  end
data/plugins/task.rb CHANGED
@@ -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
 
@@ -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
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hiiro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.29
4
+ version: 0.1.31
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota