Class: AgentsControl::ProcessProbe

Inherits:
Object
  • Object
show all
Defined in:
lib/agents_control/process_probe.rb

Overview

Who's actually running in each tab.

A tab's title can't be trusted: Claude Code sets it via an OSC sequence, and the title stays up after the process exits.

One ps -A call for the whole process tree instead of a separate ps -t per tab.

Defined Under Namespace

Classes: Process

Constant Summary collapse

NO_TTY =

Processes with no controlling terminal. This is what an agent session launched from the VS Code extension looks like: it has no tab at all.

"??"
AGENT_BINARIES =

An agent is identified by the basename of its executable: in a terminal that's claude; in VS Code it's a long path into the extension's directory.

{
  "claude" => :claude_code,
  "codex" => :codex
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(executor: Executor.new) ⇒ ProcessProbe

Returns a new instance of ProcessProbe.



35
36
37
# File 'lib/agents_control/process_probe.rb', line 35

def initialize(executor: Executor.new)
  @executor = executor
end

Instance Method Details

#agent_in(tty) ⇒ Object

The live agent in a tab — or nil, whatever the title claims.



63
64
65
# File 'lib/agents_control/process_probe.rb', line 63

def agent_in(tty)
  for_tty(tty).find(&:agent?)
end

#cwds(pids) ⇒ Object

Working directories of processes: { pid => path }.

For a tabless session this is the only way to give it a human name — there's no title and no cwd from a terminal. One lsof call for all pids at once (0.027s), not one call per pid.



87
88
89
90
91
92
93
94
95
96
97
# File 'lib/agents_control/process_probe.rb', line 87

def cwds(pids)
  pids = Array(pids).compact.uniq
  return {} if pids.empty?

  lsof = Which.find("lsof")
  return {} unless lsof

  result = @executor.run(lsof, "-a", "-d", "cwd", "-p", pids.join(","), "-Fn")
  # We may lack permission for someone else's processes — that's fine, return what we have.
  result.success? ? parse_lsof(result.stdout) : {}
end

#for_tty(tty) ⇒ Object

A tab's processes, in launch order.



52
53
54
# File 'lib/agents_control/process_probe.rb', line 52

def for_tty(tty)
  by_tty.fetch(normalize(tty), [])
end

#foreground(tty) ⇒ Object

What's currently in the foreground of a tab. This is what the user sees.



57
58
59
60
# File 'lib/agents_control/process_probe.rb', line 57

def foreground(tty)
  candidates = for_tty(tty)
  candidates.reverse.find(&:foreground?) || candidates.last
end

#processesObject



47
48
49
# File 'lib/agents_control/process_probe.rb', line 47

def processes
  @processes || refresh.processes
end

#refreshObject

Re-reads the process tree. The result is cached on the object — one probe per registry pass.



41
42
43
44
45
# File 'lib/agents_control/process_probe.rb', line 41

def refresh
  result = @executor.run(Which.find!("ps"), "-A", "-o", "tty=,pid=,stat=,comm=")
  @processes = result.success? ? parse(result.stdout) : []
  self
end

#running?(name) ⇒ Boolean

Whether an app with this name is running.

Deliberately not via pgrep: on macOS pgrep -x matches against the full path rather than the name, and in a restricted environment pgrep can miss processes that ps sees fine. We already have the list — this is a filter over existing data, not another call.

Returns:

  • (Boolean)


73
74
75
# File 'lib/agents_control/process_probe.rb', line 73

def running?(name)
  processes.any? { |process| process.name == name }
end

#terminalless_agentsObject

Agents with no tab: VS Code sessions and anything else terminalless.



78
79
80
# File 'lib/agents_control/process_probe.rb', line 78

def terminalless_agents
  processes.select { |process| process.agent? && !process.terminal? }
end