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 +4 -4
- data/CHANGELOG.md +11 -0
- data/docs/t.md +2 -2
- data/lib/hiiro/herdr.rb +17 -0
- data/lib/hiiro/task_cli.rb +72 -11
- data/lib/hiiro/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4910c0070a20706820a27abe45f728d8751da4d9d2297cacd05d26836c96dce1
|
|
4
|
+
data.tar.gz: 3173f0e134ef9353383405aff0dfd72ebe502a72ec3a2f149000dd6d221e6116
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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))
|
data/lib/hiiro/task_cli.rb
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
471
|
-
# in the label only, so the choice still maps to the exact
|
|
472
|
-
def pick_switch_target(tasks, workspaces)
|
|
473
|
-
|
|
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::
|
|
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