title: improve agent CLI detection for sparkle indicator
parent
7b8a4ffceb
commit
f1dc33f167
|
|
@ -389,8 +389,8 @@ extension Ghostty {
|
|||
|
||||
// Poll the foreground process name periodically so we can update UI
|
||||
// indicators (tab title prefix) even if the terminal title itself
|
||||
// doesn't change.
|
||||
startAgentRunningTimer()
|
||||
// doesn't change. This is only enabled when configured.
|
||||
updateAgentRunningTimer()
|
||||
|
||||
// Setup our tracking area so we get mouse moved events
|
||||
updateTrackingAreas()
|
||||
|
|
@ -446,6 +446,22 @@ extension Ghostty {
|
|||
refreshAgentRunning()
|
||||
}
|
||||
|
||||
private func stopAgentRunningTimer() {
|
||||
agentRunningTimer?.invalidate()
|
||||
agentRunningTimer = nil
|
||||
|
||||
if agentRunning { agentRunning = false }
|
||||
}
|
||||
|
||||
private func updateAgentRunningTimer() {
|
||||
let enabled = (NSApplication.shared.delegate as? AppDelegate)?.ghostty.config.titleAgentIndicator ?? false
|
||||
if enabled {
|
||||
startAgentRunningTimer()
|
||||
} else {
|
||||
stopAgentRunningTimer()
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshAgentRunning() {
|
||||
guard let surface else {
|
||||
if agentRunning { agentRunning = false }
|
||||
|
|
@ -737,6 +753,7 @@ extension Ghostty {
|
|||
// Update our derived config
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.derivedConfig = DerivedConfig(config)
|
||||
self?.updateAgentRunningTimer()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -942,13 +942,7 @@ pub fn agentCliRunning(self: *Surface) bool {
|
|||
},
|
||||
};
|
||||
|
||||
var name_buf: [256]u8 = undefined;
|
||||
const name = foreground_process.foregroundProcessNameFromPtyMaster(
|
||||
pty_master_fd,
|
||||
name_buf[0..],
|
||||
) orelse return false;
|
||||
|
||||
return foreground_process.isAgentCliProcessName(name);
|
||||
return foreground_process.isAgentCliFromPtyMaster(pty_master_fd);
|
||||
}
|
||||
|
||||
/// Called from the app thread to handle mailbox messages to our specific
|
||||
|
|
|
|||
|
|
@ -5,11 +5,18 @@ const posix = std.posix;
|
|||
|
||||
const c = if (builtin.os.tag == .windows) struct {} else @cImport({
|
||||
@cInclude("sys/ioctl.h");
|
||||
@cInclude("sys/sysctl.h");
|
||||
@cInclude("errno.h");
|
||||
});
|
||||
|
||||
// macOS: process name lookup helper (libproc).
|
||||
extern fn proc_name(pid: c_int, buffer: [*]u8, buffersize: u32) callconv(.c) c_int;
|
||||
|
||||
pub fn isAgentCliFromPtyMaster(pty_master_fd: posix.fd_t) bool {
|
||||
const pgid = foregroundProcessGroupIdFromPtyMaster(pty_master_fd) orelse return false;
|
||||
return isAgentCliPid(pgid);
|
||||
}
|
||||
|
||||
/// Returns the basename of the process that is currently in the foreground
|
||||
/// process group for the PTY associated with `pty_master_fd`.
|
||||
///
|
||||
|
|
@ -23,11 +30,18 @@ pub fn foregroundProcessNameFromPtyMaster(
|
|||
if (comptime builtin.os.tag == .ios) return null;
|
||||
if (buf.len == 0) return null;
|
||||
|
||||
const pgid = foregroundProcessGroupIdFromPtyMaster(pty_master_fd) orelse return null;
|
||||
return processName(pgid, buf);
|
||||
}
|
||||
|
||||
pub fn foregroundProcessGroupIdFromPtyMaster(pty_master_fd: posix.fd_t) ?std.c.pid_t {
|
||||
if (comptime builtin.os.tag == .windows) return null;
|
||||
if (comptime builtin.os.tag == .ios) return null;
|
||||
|
||||
var pgid: std.c.pid_t = 0;
|
||||
if (c.ioctl(pty_master_fd, c.TIOCGPGRP, @intFromPtr(&pgid)) < 0) return null;
|
||||
if (pgid <= 0) return null;
|
||||
|
||||
return processName(pgid, buf);
|
||||
return pgid;
|
||||
}
|
||||
|
||||
/// Returns the basename of the process with the given pid, if it can be
|
||||
|
|
@ -54,8 +68,11 @@ pub fn isAgentCliProcessName(name: []const u8) bool {
|
|||
// Note: these are prefix matches (case-insensitive) because many
|
||||
// package managers install with suffixes like `-cli` or `-code`.
|
||||
"gemini",
|
||||
"gemini-cli",
|
||||
"codex",
|
||||
"openai-codex",
|
||||
"claude",
|
||||
"claude-code",
|
||||
};
|
||||
|
||||
for (known_prefixes) |k| {
|
||||
|
|
@ -65,6 +82,87 @@ pub fn isAgentCliProcessName(name: []const u8) bool {
|
|||
return false;
|
||||
}
|
||||
|
||||
fn isAgentCliPid(pid: std.c.pid_t) bool {
|
||||
var name_buf: [256]u8 = undefined;
|
||||
if (processName(pid, name_buf[0..])) |name| {
|
||||
if (isAgentCliProcessName(name)) return true;
|
||||
}
|
||||
|
||||
var cmdline_buf: [16 * 1024]u8 = undefined;
|
||||
const cmdline = processCommandLine(pid, cmdline_buf[0..]) orelse return false;
|
||||
return isAgentCliCmdline(cmdline);
|
||||
}
|
||||
|
||||
fn isAgentCliCmdline(cmdline: []const u8) bool {
|
||||
return switch (comptime builtin.os.tag) {
|
||||
.linux => isAgentCliNullSeparatedArgs(cmdline, null),
|
||||
.macos => isAgentCliMacosProcargs(cmdline),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn isAgentCliNullSeparatedArgs(data: []const u8, max_args: ?usize) bool {
|
||||
var count: usize = 0;
|
||||
var i: usize = 0;
|
||||
while (i < data.len) {
|
||||
// Skip NUL separators.
|
||||
while (i < data.len and data[i] == 0) i += 1;
|
||||
if (i >= data.len) break;
|
||||
|
||||
const start = i;
|
||||
while (i < data.len and data[i] != 0) i += 1;
|
||||
const arg = data[start..i];
|
||||
if (arg.len != 0 and isAgentCliArg(arg)) return true;
|
||||
|
||||
count += 1;
|
||||
if (max_args) |m| if (count >= m) break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn isAgentCliMacosProcargs(data: []const u8) bool {
|
||||
if (data.len < @sizeOf(c_int)) return false;
|
||||
|
||||
// Format: int argc; exec_path\0 ... argv[0]\0 argv[1]\0 ... env...
|
||||
const argc: usize = @intCast(std.mem.readInt(
|
||||
c_int,
|
||||
data[0..@sizeOf(c_int)],
|
||||
builtin.cpu.arch.endian(),
|
||||
));
|
||||
var i: usize = @sizeOf(c_int);
|
||||
|
||||
// exec path
|
||||
const exec_start = i;
|
||||
while (i < data.len and data[i] != 0) i += 1;
|
||||
if (i > exec_start) {
|
||||
const exec_path = data[exec_start..i];
|
||||
if (isAgentCliArg(exec_path)) return true;
|
||||
}
|
||||
|
||||
// Skip NUL padding to argv region
|
||||
while (i < data.len and data[i] == 0) i += 1;
|
||||
|
||||
// Parse argv strings (bounded by argc)
|
||||
return isAgentCliNullSeparatedArgs(data[i..], argc);
|
||||
}
|
||||
|
||||
fn isAgentCliArg(arg: []const u8) bool {
|
||||
const base = std.fs.path.basename(arg);
|
||||
return isAgentCliProcessName(base);
|
||||
}
|
||||
|
||||
fn processCommandLine(pid: std.c.pid_t, buf: []u8) ?[]const u8 {
|
||||
if (buf.len == 0) return null;
|
||||
if (pid <= 0) return null;
|
||||
|
||||
return switch (comptime builtin.os.tag) {
|
||||
.linux => linuxCmdline(pid, buf),
|
||||
.macos => macosProcargs(pid, buf),
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
fn linuxProcessName(pid: std.c.pid_t, buf: []u8) ?[]const u8 {
|
||||
var path_buf: [64]u8 = undefined;
|
||||
const path = std.fmt.bufPrint(&path_buf, "/proc/{d}/comm", .{pid}) catch return null;
|
||||
|
|
@ -84,3 +182,28 @@ fn macosProcessName(pid: std.c.pid_t, buf: []u8) ?[]const u8 {
|
|||
|
||||
return std.mem.sliceTo(buf[0..@intCast(rc)], 0);
|
||||
}
|
||||
|
||||
fn linuxCmdline(pid: std.c.pid_t, buf: []u8) ?[]const u8 {
|
||||
var path_buf: [64]u8 = undefined;
|
||||
const path = std.fmt.bufPrint(&path_buf, "/proc/{d}/cmdline", .{pid}) catch return null;
|
||||
|
||||
const file = std.fs.openFileAbsolute(path, .{}) catch return null;
|
||||
defer file.close();
|
||||
|
||||
const n = file.readAll(buf) catch return null;
|
||||
if (n == 0) return null;
|
||||
return buf[0..n];
|
||||
}
|
||||
|
||||
fn macosProcargs(pid: std.c.pid_t, buf: []u8) ?[]const u8 {
|
||||
var mib = [_]c_int{
|
||||
c.CTL_KERN,
|
||||
c.KERN_PROCARGS2,
|
||||
@intCast(pid),
|
||||
};
|
||||
|
||||
var size: usize = buf.len;
|
||||
if (c.sysctl(&mib, mib.len, buf.ptr, &size, null, 0) != 0) return null;
|
||||
if (size == 0) return null;
|
||||
return buf[0..size];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue