hiiro 0.1.369 → 0.1.371

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: 3565b794ff2dd7e61d45810479a5bed58621c54ac4e3afa51f12adeb9626c639
4
- data.tar.gz: 75073d800fee5ab0185eea6062b39961b7f68c4200a1f428f8ac72f090edabd5
3
+ metadata.gz: 4910c0070a20706820a27abe45f728d8751da4d9d2297cacd05d26836c96dce1
4
+ data.tar.gz: 3173f0e134ef9353383405aff0dfd72ebe502a72ec3a2f149000dd6d221e6116
5
5
  SHA512:
6
- metadata.gz: 9435184f96a4b71ea43ecb873180411326aaf5ebb866153748be8f9e5fdbe7605d92c71df4506498029772f510374e91034ce74748663a9d1b4de8ff264d5c17
7
- data.tar.gz: 8a8a5539086877c051ce05639fdb2c72a626c88d92284981c551fff2a3b6a8d6e048250d969b6fedb4e811c720da0a4c58a1e6aa97dddebeba1a478f1c26194e
6
+ metadata.gz: e0fb10ffa72f026a25c0cba9ebe6f926f7d6c7504ea1d51f729519feed61918cba7fb2cf0da814c322f787c90ec532b667ea22c9e2c3bbfacb414758fb30cbc8
7
+ data.tar.gz: 5192581c3c17c73e5d7781f3cfd736e16678d89be171ca1676dfedd7e49c53e007bef1efe5b1f80bf61e827113fad82b07206564ce9779ccc97a1cbec28f4449
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.1.371] - 2026-09-14
6
+
7
+ ### Added
8
+ - `t ls` lists the panes under every open workspace with directory, agent state, and foreground command via `herdr pane process-info`; `t switch` accepts an exact pane ID and its picker includes panes
9
+ - `Hiiro::Herdr#process_info` and `Pane#foreground_command`
10
+
11
+ ## [0.1.370] - 2026-09-14
12
+
13
+ ### Added
14
+ - `t ls` marks tasks with an open Herdr workspace with `@` and lists workspaces that belong to no task
15
+
5
16
  ## [0.1.369] - 2026-09-14
6
17
 
7
18
  ### Added
data/docs/t.md CHANGED
@@ -29,7 +29,7 @@ Task names may be `parent/child` for subtasks. `t new NAME` creates exactly `NAM
29
29
 
30
30
  | Command | Behavior |
31
31
  |---|---|
32
- | `t`, `t ls`, `t list` | Lists all tasks alphabetically with open todo counts, e.g. `prez (3)`, plus `next:` and `waiting:` text |
32
+ | `t`, `t ls`, `t list` | Lists all tasks alphabetically with open todo counts, e.g. `prez (3)`, plus `next:` and `waiting:` text. When Herdr is running, tasks whose workspace is open get an `@` marker, every open workspace lists its panes as `ID ~/dir agent (status) command` (from `herdr pane process-info`), and live workspaces that belong to no task are listed afterwards with their IDs |
33
33
  | `t show [TASK]` | Status, home, next action, waiting text, code directory, workspace label, resources, documents, and todos |
34
34
  | `t current [TASK]` | Prints the resolved task name; never saves |
35
35
  | `t use TASK` (alias `pin`) | Saves TASK as the fallback for when no workspace or directory identifies a task |
@@ -112,7 +112,7 @@ t pane run [TASK] ID|LABEL [--] COMMAND...
112
112
  t pane split [TASK] ID|LABEL [--direction right|down] [--directory PATH] [--command COMMAND]
113
113
  ```
114
114
 
115
- These require a running Herdr server. The workspace label is the task session or name with `.` replaced by `_`. `switch` also accepts the name of a live Herdr workspace that belongs to no task, exactly or by unique prefix, and focuses it; a task with the same name wins. When the name is ambiguous, or no task or context is given, a terminal gets a fuzzy finder over tasks and loose workspaces, with duplicate workspace names numbered in the label only. `switch` focuses the existing task workspace or creates it in the start directory (`--directory`, else primary directory, worktree, or home), and saves the task as the fallback when it was named explicitly. `--show` only prints the workspace, tabs, and panes. Tab and pane references match a live ID or label, then a unique prefix; with no reference and several candidates a fuzzy finder opens.
115
+ These require a running Herdr server. The workspace label is the task session or name with `.` replaced by `_`. `switch` also accepts the name of a live Herdr workspace that belongs to no task, exactly or by unique prefix, and focuses it; a task with the same name wins. An exact pane ID such as `w6:p2` focuses that pane, and the picker lists every live pane with its directory and foreground command. When the name is ambiguous, or no task or context is given, a terminal gets a fuzzy finder over tasks and loose workspaces, with duplicate workspace names numbered in the label only. `switch` focuses the existing task workspace or creates it in the start directory (`--directory`, else primary directory, worktree, or home), and saves the task as the fallback when it was named explicitly. `--show` only prints the workspace, tabs, and panes. Tab and pane references match a live ID or label, then a unique prefix; with no reference and several candidates a fuzzy finder opens.
116
116
 
117
117
  ## Native AI sessions
118
118
 
data/lib/hiiro/herdr.rb CHANGED
@@ -204,6 +204,19 @@ class Hiiro
204
204
  def to_s
205
205
  "#{id} #{name} [#{workspace_id} / #{tab_id}]#{focused? ? ' *' : ''}"
206
206
  end
207
+
208
+ # `herdr pane process-info` for this pane, fetched once.
209
+ def process_info
210
+ @process_info ||= client.process_info(id)
211
+ end
212
+
213
+ # Command line of the foreground process group leader, or nil at a shell prompt.
214
+ def foreground_command
215
+ procs = process_info['foreground_processes'] || []
216
+ leader = procs.find { |proc| proc['pid'] == process_info['foreground_process_group_id'] } || procs.last
217
+ return nil if leader.nil? || leader['pid'] == process_info['shell_pid']
218
+ leader['cmdline']
219
+ end
207
220
  end
208
221
 
209
222
  class Panes < Collection
@@ -319,6 +332,10 @@ class Hiiro
319
332
  row && Tab.new(row, client: self)
320
333
  end
321
334
 
335
+ def process_info(pane_id)
336
+ capture_result('pane', 'process-info', '--pane', pane_id).fetch('process_info', {})
337
+ end
338
+
322
339
  def get_pane(id)
323
340
  row = capture_result('pane', 'get', id)['pane']
324
341
  row && Pane.new(row, client: self, rect: pane_rect(id))
@@ -40,7 +40,6 @@ class Hiiro
40
40
  add_flag :find, short: :f, desc: 'Choose the task with a fuzzy finder'
41
41
  end
42
42
 
43
- # Exact name, else unique prefix; ambiguous prefixes fail; nil when nothing matches.
44
43
  # Exact name, else unique prefix. An ambiguous prefix opens the fuzzy finder
45
44
  # over the candidates on a TTY and fails otherwise. nil when nothing matches.
46
45
  def lookup_task(reference)
@@ -180,24 +179,73 @@ class Hiiro
180
179
  end
181
180
  end
182
181
 
182
+ # Live Herdr workspaces, or [] when Herdr is not running.
183
+ def live_workspaces
184
+ herdr_client.server_running? ? client.workspaces.to_a : []
185
+ rescue Hiiro::Error
186
+ []
187
+ end
188
+
189
+ # All live panes grouped by workspace id, or {} when Herdr is not running.
190
+ def live_panes
191
+ @live_panes ||= begin
192
+ herdr_client.server_running? ? client.panes(all: true).to_a.group_by(&:workspace_id) : {}
193
+ rescue Hiiro::Error
194
+ {}
195
+ end
196
+ end
197
+
198
+ # "ID ~/dir agent (status) command line" for listings and pickers.
199
+ def pane_line(pane)
200
+ dir = (pane.foreground_cwd || pane.cwd).to_s.sub(/\A#{Regexp.escape(Dir.home)}(?=\/|\z)/, '~')
201
+ what = []
202
+ what << "#{pane.agent}#{pane.agent_status ? " (#{pane.agent_status})" : ''}" if pane.agent
203
+ what << (pane.foreground_command || (pane.agent ? nil : 'shell'))
204
+ [pane.id, dir, *what.compact].reject(&:empty?).join(' ')
205
+ end
206
+
207
+ def print_panes(workspace_id, indent: ' ')
208
+ live_panes.fetch(workspace_id, []).each { |pane| puts "#{indent}#{pane_line(pane)}" }
209
+ end
210
+
211
+ # Tasks with open todo counts, an @ marker when the task workspace is open,
212
+ # then any live Herdr workspaces that belong to no task. Panes are listed
213
+ # under every open workspace with their directory and foreground command.
183
214
  def list_tasks
184
215
  tasks = Hiiro::TaskRecord.all_as_list
185
- if tasks.empty?
216
+ workspaces = live_workspaces
217
+ labels = tasks.map { |task| workspace_label(task) }
218
+ open_labels = workspaces.map(&:name)
219
+ loose = workspaces.reject { |workspace| labels.include?(workspace.name) }
220
+ if tasks.empty? && loose.empty?
186
221
  puts 'No tasks. Create one with t new NAME.'
187
222
  return
188
223
  end
189
224
  counts = open_todo_counts
225
+ attached = tasks.any? { |task| open_labels.include?(workspace_label(task)) }
190
226
  rows = tasks.map do |task|
191
227
  count = counts[task.name]
192
228
  label = count.zero? ? task.name : "#{task.name} (#{count})"
229
+ label = "#{open_labels.include?(workspace_label(task)) ? '@' : ' '} #{label}" if attached
193
230
  detail = []
194
231
  detail << "next: #{task.next_action}" if task.next_action
195
232
  detail << "waiting: #{task.waiting_on}" if task.waiting_on
196
233
  [label, task.task_status, detail.join(' ')]
197
234
  end
198
- name_col = rows.map { |row| row[0].length }.max
199
- status_col = rows.map { |row| row[1].length }.max
200
- rows.each { |label, status, detail| puts format("%-#{name_col}s %-#{status_col}s %s", label, status, detail).rstrip }
235
+ name_col = rows.map { |row| row[0].length }.max || 0
236
+ status_col = rows.map { |row| row[1].length }.max || 0
237
+ rows.each_with_index do |(label, status, detail), index|
238
+ puts format("%-#{name_col}s %-#{status_col}s %s", label, status, detail).rstrip
239
+ workspace = workspaces.find { |candidate| candidate.name == workspace_label(tasks[index]) }
240
+ print_panes(workspace.id) if workspace
241
+ end
242
+ return if loose.empty?
243
+ puts unless rows.empty?
244
+ puts 'Workspaces without a task:'
245
+ loose.each do |workspace|
246
+ puts " #{workspace.name} #{workspace.id}"
247
+ print_panes(workspace.id)
248
+ end
201
249
  end
202
250
 
203
251
 
@@ -467,10 +515,11 @@ class Hiiro
467
515
  client.workspaces.reject { |workspace| labels.include?(workspace.name) }
468
516
  end
469
517
 
470
- # Fuzzy map over tasks and loose workspaces; duplicate workspace names are numbered
471
- # in the label only, so the choice still maps to the exact workspace.
472
- def pick_switch_target(tasks, workspaces)
473
- raise Error, 'No tasks or workspaces to choose from' if tasks.empty? && workspaces.empty?
518
+ # Fuzzy map over tasks, loose workspaces, and live panes; duplicate workspace
519
+ # names are numbered in the label only, so the choice still maps to the exact one.
520
+ def pick_switch_target(tasks, workspaces, panes: nil)
521
+ panes = live_panes.values.flatten if panes.nil?
522
+ raise Error, 'No tasks, workspaces, or panes to choose from' if tasks.empty? && workspaces.empty? && panes.empty?
474
523
  counts = workspaces.group_by(&:name).transform_values(&:length)
475
524
  seen = Hash.new(0)
476
525
  map = tasks.to_h { |task| [task.name, task] }
@@ -479,9 +528,16 @@ class Hiiro
479
528
  label = counts[workspace.name] > 1 ? "#{workspace.name} ##{seen[workspace.name]}" : workspace.name
480
529
  map["#{label} (workspace)"] = workspace
481
530
  end
531
+ panes.each { |pane| map["#{pane_line(pane)} (pane)"] = pane }
482
532
  fuzzyfind_from_map(map) || raise(Error, 'Nothing selected')
483
533
  end
484
534
 
535
+ def focus_pane_target(pane)
536
+ check_result(client.focus_workspace(pane.workspace_id))
537
+ check_result(client.focus_pane(pane.id), "Could not focus pane #{pane.id}")
538
+ puts pane_line(pane)
539
+ end
540
+
485
541
  # [target, explicit]. A task wins over a workspace with the same name.
486
542
  def switch_target(args)
487
543
  args = args.dup
@@ -495,6 +551,8 @@ class Hiiro
495
551
  workspaces = loose_workspaces
496
552
  exact = tasks.find { |task| task.name == first }
497
553
  return [exact, true] if exact
554
+ pane = live_panes.values.flatten.find { |candidate| candidate.id == first }
555
+ return [pane, false] if pane
498
556
  exact_ws = workspaces.select { |workspace| workspace.name == first }
499
557
  return [exact_ws.first, false] if exact_ws.one?
500
558
  task_matches = tasks.select { |task| task.name.start_with?(first) }
@@ -504,7 +562,7 @@ class Hiiro
504
562
  raise Error, "No task or workspace matches #{first}" if task_matches.empty? && ws_matches.empty?
505
563
  names = (task_matches.map(&:name) + ws_matches.map { |w| "#{w.name} (workspace)" }).join(', ')
506
564
  raise Error, "Ambiguous #{first}: #{names}" unless $stdin.tty?
507
- target = pick_switch_target(task_matches, ws_matches)
565
+ target = pick_switch_target(task_matches, ws_matches, panes: [])
508
566
  return [target, target.is_a?(Hiiro::TaskRecord)]
509
567
  end
510
568
  args.shift if first == '.'
@@ -738,7 +796,10 @@ class Hiiro
738
796
  add_option :directory, desc: 'Existing start directory'
739
797
  add_cmd(:switch, :workspace, args: ['task-or-workspace?'], opts: [*TASK_OPTIONS, :directory, :show]) do
740
798
  target, explicit = switch_target(opts.args)
741
- if target.is_a?(Hiiro::Herdr::Workspace)
799
+ if target.is_a?(Hiiro::Herdr::Pane)
800
+ raise Error, '--show applies to task workspaces' if opts.show
801
+ focus_pane_target(target)
802
+ elsif target.is_a?(Hiiro::Herdr::Workspace)
742
803
  raise Error, '--show applies to task workspaces' if opts.show
743
804
  check_result(client.focus_workspace(target.id))
744
805
  puts target
data/lib/hiiro/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  class Hiiro
2
- VERSION = "0.1.369"
2
+ VERSION = "0.1.371"
3
3
  SUPPORTED_RUBY_VERSION = ">= 3.2.0"
4
4
  end
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.369
4
+ version: 0.1.371
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota