tmuxclihook 0.5.7 → 0.5.8
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.
- package/package.json +1 -1
- package/scripts/lib-tmux-ai-status.sh +87 -23
- package/scripts/tmux-ai-monitor +53 -20
package/package.json
CHANGED
|
@@ -684,31 +684,95 @@ _pane_command_is_tool() { # $1=pane_id $2=工具名
|
|
|
684
684
|
_is_tool_process_name "$(basename "$current_command" 2>/dev/null)" "$2"
|
|
685
685
|
}
|
|
686
686
|
|
|
687
|
+
# 全量进程快照(一次 ps):每行 "pid ppid comm"。供多次进程树 BFS 复用。
|
|
688
|
+
# 旧的逐 pid `ps -o comm=` + `pgrep -P` 实现在 pane 多的 tick 内会放大到秒级
|
|
689
|
+
# (monitor 每 tick 要对每个 launcher pane × 每个工具做一次),改为一次快照 + 内存 BFS。
|
|
690
|
+
_ps_process_snapshot() {
|
|
691
|
+
ps -axo pid=,ppid=,comm= 2>/dev/null
|
|
692
|
+
}
|
|
693
|
+
|
|
687
694
|
# 从给定 pid 出发 BFS,匹配进程名集合(空格分隔,前缀匹配)。
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
local
|
|
695
|
+
# 名称前缀匹配语义同 _is_tool_process_name(codex 匹配 codex-aarch64-*)。
|
|
696
|
+
_pid_tree_has_names() { # $1=pane_pid $2=names [$3=进程快照]
|
|
697
|
+
local pane_pid="$1" names="$2" snapshot="${3:-}"
|
|
691
698
|
[ -n "$pane_pid" ] || return 1
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
699
|
+
[ -n "$snapshot" ] || snapshot=$(_ps_process_snapshot)
|
|
700
|
+
[ -n "$snapshot" ] || return 1
|
|
701
|
+
printf '%s\n' "$snapshot" | awk -v root="$pane_pid" -v names="$names" '
|
|
702
|
+
{ cmd[$1] = $3; if ($2 != "") children[$2] = children[$2] " " $1 }
|
|
703
|
+
END {
|
|
704
|
+
n = split(names, want, " ")
|
|
705
|
+
frontier = root
|
|
706
|
+
depth = 0
|
|
707
|
+
while (frontier != "" && depth < 10) {
|
|
708
|
+
m = split(frontier, pids, " ")
|
|
709
|
+
nxt = ""
|
|
710
|
+
for (i = 1; i <= m; i++) {
|
|
711
|
+
base = cmd[pids[i]]
|
|
712
|
+
sub(/.*\//, "", base)
|
|
713
|
+
for (j = 1; j <= n; j++) {
|
|
714
|
+
nm = want[j]
|
|
715
|
+
if (base == nm \
|
|
716
|
+
|| substr(base, 1, length(nm) + 1) == nm "-" \
|
|
717
|
+
|| substr(base, 1, length(nm) + 1) == nm "_") exit 0
|
|
718
|
+
}
|
|
719
|
+
c = split(children[pids[i]], kids, " ")
|
|
720
|
+
for (k = 1; k <= c; k++) {
|
|
721
|
+
if (kids[k] == "") continue
|
|
722
|
+
nxt = nxt (nxt == "" ? "" : " ") kids[k]
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
frontier = nxt
|
|
726
|
+
depth++
|
|
727
|
+
}
|
|
728
|
+
exit 1
|
|
729
|
+
}'
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
# 批量版:一次快照 + 一次 awk,判定每个 root × 每个 name。
|
|
733
|
+
# 输出命中行 "<pid><TAB><name>";monitor 用它把多个 pane 的存活探测合并成一次
|
|
734
|
+
# awk(逐 pane 调用 _pid_tree_has_names 会重复解析同一份快照)。
|
|
735
|
+
_pid_trees_hits() { # $1=起点 pid 列表 $2=进程名列表 [$3=进程快照]
|
|
736
|
+
local roots="$1" names="$2" snapshot="${3:-}"
|
|
737
|
+
[ -n "$roots" ] && [ -n "$names" ] || return 0
|
|
738
|
+
[ -n "$snapshot" ] || snapshot=$(_ps_process_snapshot)
|
|
739
|
+
[ -n "$snapshot" ] || return 0
|
|
740
|
+
printf '%s\n' "$snapshot" | awk -v roots="$roots" -v names="$names" '
|
|
741
|
+
{ cmd[$1] = $3; if ($2 != "") children[$2] = children[$2] " " $1 }
|
|
742
|
+
END {
|
|
743
|
+
nr = split(roots, r, " ")
|
|
744
|
+
nn = split(names, n, " ")
|
|
745
|
+
for (i = 1; i <= nr; i++)
|
|
746
|
+
for (j = 1; j <= nn; j++)
|
|
747
|
+
if (_tree_has(r[i], n[j])) print r[i] "\t" n[j]
|
|
748
|
+
}
|
|
749
|
+
function _tree_has(root, want, frontier, nxt, depth, m, pids, i, k, base, c, kids) {
|
|
750
|
+
frontier = root
|
|
751
|
+
depth = 0
|
|
752
|
+
while (frontier != "" && depth < 10) {
|
|
753
|
+
m = split(frontier, pids, " ")
|
|
754
|
+
nxt = ""
|
|
755
|
+
for (i = 1; i <= m; i++) {
|
|
756
|
+
base = cmd[pids[i]]
|
|
757
|
+
sub(/.*\//, "", base)
|
|
758
|
+
if (base == want \
|
|
759
|
+
|| substr(base, 1, length(want) + 1) == want "-" \
|
|
760
|
+
|| substr(base, 1, length(want) + 1) == want "_") return 1
|
|
761
|
+
c = split(children[pids[i]], kids, " ")
|
|
762
|
+
for (k = 1; k <= c; k++) {
|
|
763
|
+
if (kids[k] == "") continue
|
|
764
|
+
nxt = nxt (nxt == "" ? "" : " ") kids[k]
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
frontier = nxt
|
|
768
|
+
depth++
|
|
769
|
+
}
|
|
770
|
+
return 0
|
|
771
|
+
}'
|
|
708
772
|
}
|
|
709
773
|
|
|
710
774
|
# BFS pane 进程树,匹配给定进程名集合(空格分隔,前缀匹配)。
|
|
711
|
-
_pane_has_process_names() { # $1=pane_id $2=names
|
|
775
|
+
_pane_has_process_names() { # $1=pane_id $2=names [$3=进程快照]
|
|
712
776
|
local pane_id="$1" pane_pid
|
|
713
777
|
# 注意:list-panes -t "%N" 会返回该 pane 所在 window 的全部 pane,
|
|
714
778
|
# 直接取 #{pane_pid} 会混入同 window 其他 pane 的 pid,导致 BFS 爬到
|
|
@@ -716,13 +780,13 @@ _pane_has_process_names() { # $1=pane_id $2=names
|
|
|
716
780
|
pane_pid=$(tmux list-panes -a -F "#{pane_id} #{pane_pid}" 2>/dev/null \
|
|
717
781
|
| awk -v id="$pane_id" '$1==id{print $2; exit}')
|
|
718
782
|
[ -z "$pane_pid" ] && return 1
|
|
719
|
-
_pid_tree_has_names "$pane_pid" "$2"
|
|
783
|
+
_pid_tree_has_names "$pane_pid" "$2" "${3:-}"
|
|
720
784
|
}
|
|
721
785
|
|
|
722
786
|
# 判定 pane 进程树内是否仍有该工具进程(前台命令优先,再 BFS 遍历进程树)
|
|
723
|
-
_pane_has_tool_process() { # $1=pane_id $2=工具名
|
|
787
|
+
_pane_has_tool_process() { # $1=pane_id $2=工具名 [$3=进程快照]
|
|
724
788
|
_pane_command_is_tool "$1" "$2" && return 0
|
|
725
|
-
_pane_has_process_names "$1" "$2"
|
|
789
|
+
_pane_has_process_names "$1" "$2" "${3:-}"
|
|
726
790
|
}
|
|
727
791
|
|
|
728
792
|
# 所有 adapter 汇总的 AI 进程名
|
package/scripts/tmux-ai-monitor
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# SessionStart 还会延迟到首个 turn。本监控器每秒刷新:
|
|
6
6
|
# - 前台启动此类工具、尚无 hook 状态时,显示 idle `-`
|
|
7
7
|
# - cc-switch 的 `switch` 启动时前台命令是 volta-shim/node 而非工具名,改为
|
|
8
|
-
#
|
|
8
|
+
# 每 tick 探测 launcher pane 的进程树来识别,并写 seen 标记供聚合补 `-`
|
|
9
9
|
# - 退出返回 shell(或进程树已无该工具)后,清除残留 pane 状态
|
|
10
10
|
# 泛化实现:扫描 adapters/*.sh 找出 ADAPTER_HAS_SESSION_END="false" 的工具及其
|
|
11
11
|
# ADAPTER_PROCESS_NAMES,逐个工具做生命周期检测——新增同类工具的 adapter 无需改本文件。
|
|
@@ -17,8 +17,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
17
17
|
_ADAPTER_DIR="${SCRIPT_DIR}/adapters"
|
|
18
18
|
MODE="${1:-start}"
|
|
19
19
|
MONITOR_INTERVAL="${TMUX_AI_MONITOR_INTERVAL:-1}"
|
|
20
|
-
#
|
|
21
|
-
|
|
20
|
+
# 进程树探测不再跨 tick 节流:lib 用一次 ps 快照 + 一次 awk 批量判定所有 pane,
|
|
21
|
+
# 单 tick 成本 ~30ms,每 tick 探测让新启动的工具在 ≤1 个 tick(≈1s)内显示。
|
|
22
22
|
_MONITOR_TICK=0
|
|
23
23
|
# 升级自检:每 N tick 对比脚本内容指纹,变化则 exec 自身重载新代码。
|
|
24
24
|
_MONITOR_RECHECK_EVERY=60
|
|
@@ -68,31 +68,64 @@ _collect_no_session_end_tools() {
|
|
|
68
68
|
# → 标记见过;曾标记但前台已切走且进程树也无该工具 → 清空 pane 状态(该工具已退出)。
|
|
69
69
|
# seen 标记按工具名区分(@ai_monitor_seen_<name>),避免多工具互相踩踏清理语义;
|
|
70
70
|
# 该标记同时是 build_all_status 给 launcher pane 补 idle `-` 的依据。
|
|
71
|
+
#
|
|
72
|
+
# 性能:每 tick 只取一次进程快照,并把「待识别 launcher」与「已见 pane 存活确认」
|
|
73
|
+
# 合并成一次 _pid_trees_hits(一次 awk)。旧实现逐 pane 调 _pane_has_tool_process,
|
|
74
|
+
# 每个 pid 各起一次 ps/pgrep,launcher 多的环境单 tick 可达 1.8s,叠加 5 tick 节流
|
|
75
|
+
# 后新启动 codex 要 7-8s 才显示(2026-10-19 实测)。
|
|
71
76
|
_monitor_no_session_end_lifecycle() {
|
|
72
|
-
local tools name pane_id current_command
|
|
77
|
+
local tools name pane_id current_command seen attached pane_pid
|
|
73
78
|
tools=$(_collect_no_session_end_tools)
|
|
74
79
|
[ -n "$tools" ] || return 0
|
|
80
|
+
local snapshot
|
|
81
|
+
snapshot=$(_ps_process_snapshot)
|
|
75
82
|
for name in $tools; do
|
|
76
|
-
|
|
83
|
+
local probe="" watch="" roots="" hits="" hit_pid hit_name hit_pids=" "
|
|
84
|
+
local entry sanitized pane_dir
|
|
85
|
+
while IFS='|' read -r pane_id current_command seen attached pane_pid; do
|
|
86
|
+
[ -n "$pane_id" ] || continue
|
|
77
87
|
if [ -n "$current_command" ] && _is_tool_process_name "$(basename "$current_command" 2>/dev/null)" "$name"; then
|
|
88
|
+
# 前台命令即目标工具:直接标记见过(无需 BFS)
|
|
78
89
|
[ "$seen" = "1" ] || tmux set-option -pt "$pane_id" "@ai_monitor_seen_${name}" 1 2>/dev/null || true
|
|
79
|
-
elif [ "$seen" = "1" ] &&
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
pane_dir="${_STATUS_DIR}/${sanitized}"
|
|
84
|
-
rm -rf "$pane_dir" 2>/dev/null
|
|
85
|
-
elif [ "$seen" != "1" ] \
|
|
86
|
-
&& [ "${attached:-0}" -gt 0 ] \
|
|
87
|
-
&& [ $((_MONITOR_TICK % _MONITOR_PROBE_EVERY)) -eq 0 ] \
|
|
88
|
-
&& _is_launcher_command "$current_command" \
|
|
89
|
-
&& _pane_has_tool_process "$pane_id" "$name"; then
|
|
90
|
+
elif [ "$seen" = "1" ] && [ -n "$pane_pid" ]; then
|
|
91
|
+
# 曾见过:确认进程树里是否还有该工具(前台命令已在上一分支排除)
|
|
92
|
+
watch="$watch $pane_id:$pane_pid"
|
|
93
|
+
elif [ "${attached:-0}" -gt 0 ] && _is_launcher_command "$current_command" && [ -n "$pane_pid" ]; then
|
|
90
94
|
# launcher(switch/volta-shim/node)启动:前台命令不是工具名,
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
|
|
95
|
+
# 但进程树里可能有。标记 seen 后 build_all_status 才会补 idle `-`,
|
|
96
|
+
# 工具退出时也才会走清理分支(否则残留状态永不清)。
|
|
97
|
+
probe="$probe $pane_id:$pane_pid"
|
|
94
98
|
fi
|
|
95
|
-
done < <(tmux list-panes -a -F "#{pane_id}|#{pane_current_command}|#{@ai_monitor_seen_${name}}|#{session_attached}" 2>/dev/null)
|
|
99
|
+
done < <(tmux list-panes -a -F "#{pane_id}|#{pane_current_command}|#{@ai_monitor_seen_${name}}|#{session_attached}|#{pane_pid}" 2>/dev/null)
|
|
100
|
+
|
|
101
|
+
for entry in $probe $watch; do roots="$roots ${entry#*:}"; done
|
|
102
|
+
[ -n "$roots" ] && hits=$(_pid_trees_hits "$roots" "$name" "$snapshot")
|
|
103
|
+
while IFS=$'\t' read -r hit_pid hit_name; do
|
|
104
|
+
[ -n "$hit_pid" ] && hit_pids="$hit_pids$hit_pid "
|
|
105
|
+
done <<EOF
|
|
106
|
+
$hits
|
|
107
|
+
EOF
|
|
108
|
+
|
|
109
|
+
for entry in $probe; do
|
|
110
|
+
pane_pid="${entry#*:}"
|
|
111
|
+
case "$hit_pids" in
|
|
112
|
+
*" $pane_pid "*) tmux set-option -pt "${entry%%:*}" "@ai_monitor_seen_${name}" 1 2>/dev/null || true ;;
|
|
113
|
+
esac
|
|
114
|
+
done
|
|
115
|
+
for entry in $watch; do
|
|
116
|
+
pane_pid="${entry#*:}"
|
|
117
|
+
case "$hit_pids" in
|
|
118
|
+
*" $pane_pid "*) ;;
|
|
119
|
+
*)
|
|
120
|
+
pane_id="${entry%%:*}"
|
|
121
|
+
tmux set-option -pt "$pane_id" @ai_pane_status "" 2>/dev/null || true
|
|
122
|
+
tmux set-option -pu -t "$pane_id" "@ai_monitor_seen_${name}" 2>/dev/null || true
|
|
123
|
+
sanitized="${pane_id//[^a-zA-Z0-9]/_}"
|
|
124
|
+
pane_dir="${_STATUS_DIR}/${sanitized}"
|
|
125
|
+
rm -rf "$pane_dir" 2>/dev/null
|
|
126
|
+
;;
|
|
127
|
+
esac
|
|
128
|
+
done
|
|
96
129
|
done
|
|
97
130
|
}
|
|
98
131
|
|