mirror of
https://github.com/theniceboy/.config.git
synced 2026-02-04 03:53:27 +08:00
agent tracker updates
This commit is contained in:
parent
689cc061cd
commit
bb2a53206b
13 changed files with 375 additions and 106 deletions
51
.tmux.conf
51
.tmux.conf
|
|
@ -13,6 +13,7 @@ set -as terminal-features 'xterm*:extkeys'
|
|||
set -g mouse on
|
||||
set -sg exit-empty on
|
||||
set -g detach-on-destroy off
|
||||
set -g allow-passthrough on
|
||||
|
||||
set -q -g status-utf8 on
|
||||
setw -q -g utf8 on
|
||||
|
|
@ -23,8 +24,6 @@ setw -g monitor-bell off
|
|||
|
||||
set -g history-limit 10000
|
||||
|
||||
set-hook -g pane-focus-in "run -b 'bash ~/.config/tmux/fzf_panes.tmux update_mru_pane_ids'"
|
||||
|
||||
# reload configuration
|
||||
bind r source-file ~/.tmux.conf \; display '~/.tmux.conf sourced'
|
||||
|
||||
|
|
@ -38,25 +37,27 @@ XDG_SESSION_DESKTOP XDG_SESSION_TYPE XDG_CURRENT_DESKTOP \
|
|||
XMODIFIERS \
|
||||
FZF_DEFAULT_OPTS \
|
||||
TMUX_THEME_COLOR \
|
||||
TERM TERM_PROGRAM \
|
||||
'
|
||||
# set-hook -gu client-attached
|
||||
# set-hook -g client-attached 'run -b "cut -c3- ~/.tmux.conf | sh -s update_env_event"'
|
||||
# Acknowledge the current pane's task when a client attaches (graceful if tracker unavailable)
|
||||
set-hook -ag client-attached 'run -b "test -x ~/.config/agent-tracker/bin/tracker-client && ~/.config/agent-tracker/bin/tracker-client command acknowledge --client #{client_tty} --session-id #{session_id} --window-id #{window_id} --pane #{pane_id} || true"'
|
||||
|
||||
# -- Hooks (unset before setting to avoid duplicates on reload)
|
||||
set-hook -gu client-attached
|
||||
set-hook -g client-attached 'run -b "test -x ~/.config/agent-tracker/bin/tracker-client && ~/.config/agent-tracker/bin/tracker-client command acknowledge --client #{client_tty} --session-id #{session_id} --window-id #{window_id} --pane #{pane_id} || true"'
|
||||
set-hook -ag client-attached 'run -b "tmux refresh-client -S"'
|
||||
|
||||
# set-hook -gu client-resized
|
||||
# set-hook -ag client-resized 'run -b "~/.config/agent-tracker/bin/tracker-client command refresh --client #{client_tty}"'
|
||||
# set-hook -ag client-resized 'run -b "tmux refresh-client -S"'
|
||||
|
||||
set-hook -gu pane-focus-in
|
||||
set-hook -g pane-focus-in "run -b 'bash ~/.config/tmux/fzf_panes.tmux update_mru_pane_ids'"
|
||||
set-hook -ag pane-focus-in 'run -b "test -x ~/.config/agent-tracker/bin/tracker-client && ~/.config/agent-tracker/bin/tracker-client command acknowledge --client #{client_tty} --session-id #{session_id} --window-id #{window_id} --pane #{pane_id} || true"'
|
||||
set-hook -ag pane-focus-in 'run -b "tmux refresh-client -S"'
|
||||
set-hook -ag pane-died 'run -b "test -x ~/.config/agent-tracker/bin/tracker-client && ~/.config/agent-tracker/bin/tracker-client command delete_task --client #{client_tty} --session-id #{session_id} --window-id #{window_id} --pane #{pane_id} || true"'
|
||||
|
||||
set-hook -gu pane-died
|
||||
set-hook -g pane-died 'run -b "test -x ~/.config/agent-tracker/bin/tracker-client && ~/.config/agent-tracker/bin/tracker-client command delete_task --client #{client_tty} --session-id #{session_id} --window-id #{window_id} --pane #{pane_id} || true"'
|
||||
set-hook -ag pane-died 'run -b "test -x ~/.config/agent-tracker/bin/tracker-client && ~/.config/agent-tracker/bin/tracker-client command note_archive_pane --client #{client_tty} --session-id #{session_id} --window-id #{window_id} --pane #{pane_id} || true"'
|
||||
|
||||
# -- Project-specific window activation hooks
|
||||
# Checks for ./on-tmux-window-activate.sh or ../on-tmux-window-activate.sh and runs it
|
||||
set-hook -ag after-select-window 'run-shell "~/.config/tmux/scripts/check_and_run_on_activate.sh \"#{pane_current_path}\""'
|
||||
set-hook -gu after-select-window
|
||||
set-hook -g after-select-window 'run-shell "~/.config/tmux/scripts/check_and_run_on_activate.sh \"#{pane_current_path}\""'
|
||||
|
||||
# set-hook -ag window-linked 'run -b "tmux refresh-client -S"'
|
||||
# set-hook -ag window-unlinked 'run -b "tmux refresh-client -S"'
|
||||
|
|
@ -245,6 +246,8 @@ bind C-g if-shell '[[ $(tmux showw synchronize-panes | cut -d\ -f2) == "on" ]]'
|
|||
# panes
|
||||
run-shell "~/.config/tmux/scripts/update_theme_color.sh"
|
||||
setw -g pane-border-status top
|
||||
set -g pane-scrollbars on
|
||||
set -g pane-scrollbars-position right
|
||||
set -g pane-active-border-style fg=#b294bb
|
||||
set -g pane-border-style fg=colour244
|
||||
setw -g pane-border-format '#{?pane_active, #[fg=#{@theme_color}]#[bg=#{@theme_color}]#[fg=#1d1f21]#[bold] #{?window_zoomed_flag,⛶ ,} #(~/.config/tmux/scripts/pane_starship_title.sh #{pane_pid} #{pane_width} "#{pane_current_path}" "#{pane_current_command}") #[bg=default]#[fg=#{@theme_color}]#[default], #[fg=colour244]#[bg=colour244]#[fg=#1d1f21] #{?window_zoomed_flag,⛶ ,} #(~/.config/tmux/scripts/pane_starship_title.sh #{pane_pid} #{pane_width} "#{pane_current_path}" "#{pane_current_command}") #[bg=default]#[fg=colour244]#[default] }'
|
||||
|
|
@ -265,8 +268,8 @@ set -g status-bg black
|
|||
#set -g status-left '#[fg=brightyellow] #{?client_prefix,⌨ , } #[fg=magenta,bold] %Y-%m-%d %H:%M '
|
||||
#set -g status-right '#(rainbarf --battery --remaining --bolt --tmux --rgb)'
|
||||
#set -g status-left "#[fg=magenta,bold] %Y-%m-%d %H:%M | #[fg=brightblue]#(curl icanhazip.com) #(ifconfig en0 | grep 'inet ' | awk '{print \"en0 \" $2}') #(ifconfig en1 | grep 'inet ' | awk '{print \"en1 \" $2}') #(ifconfig en3 | grep 'inet ' | awk '{print \"en3 \" $2}') #(ifconfig tun0 | grep 'inet ' | awk '{print \"vpn \" $2}') "
|
||||
setw -g window-status-format '#[fg=#c5c8c6] #W '
|
||||
setw -g window-status-current-format '#[fg=#{@theme_color},bold] #W '
|
||||
setw -g window-status-format '#[fg=#c5c8c6] #W#(~/.config/tmux/tmux-status/window_task_icon.sh "#{window_id}") '
|
||||
setw -g window-status-current-format '#[fg=#{@theme_color},bold] #W#(~/.config/tmux/tmux-status/window_task_icon.sh "#{window_id}") '
|
||||
setw -g window-status-activity-style bg=black
|
||||
setw -g window-status-bell-style bg=black
|
||||
#set-window-option -g window-status-current-format "#[fg=colour235, bg=colour27]⮀#[fg=colour255, bg=colour27] #I ⮁ #W #[fg=colour27, bg=colour235]⮀"
|
||||
|
|
@ -298,3 +301,23 @@ unbind -T copy-mode-vi C-u
|
|||
unbind -T copy-mode-vi C-e
|
||||
bind -T copy-mode-vi C-u send-keys -N 5 -X scroll-up
|
||||
bind -T copy-mode-vi C-e send-keys -N 5 -X scroll-down
|
||||
|
||||
# ─── Plugins (TPM) ───────────────────────────────────────────────
|
||||
set -g @plugin 'tmux-plugins/tpm'
|
||||
set -g @plugin 'tmux-plugins/tmux-resurrect'
|
||||
set -g @plugin 'tmux-plugins/tmux-continuum'
|
||||
|
||||
# Disable TPM keybindings (managed via upgrade-all)
|
||||
set -g @tpm-install 'M-F12'
|
||||
set -g @tpm-update 'M-F12'
|
||||
set -g @tpm-clean 'M-F12'
|
||||
|
||||
# Resurrect: save pane contents, custom save key
|
||||
set -g @resurrect-capture-pane-contents 'on'
|
||||
set -g @resurrect-save 's'
|
||||
|
||||
# Continuum: auto-save every 5 minutes
|
||||
set -g @continuum-save-interval '5'
|
||||
|
||||
# Initialize TPM (keep at very bottom)
|
||||
run '~/.tmux/plugins/tpm/tpm'
|
||||
|
|
|
|||
14
agent-tracker/README.md
Normal file
14
agent-tracker/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Agent Tracker
|
||||
|
||||
Tmux-aware agent task and note tracker.
|
||||
|
||||
## Build, Install & Restart
|
||||
|
||||
```bash
|
||||
./scripts/install_brew_service.sh
|
||||
```
|
||||
|
||||
This script:
|
||||
1. Builds `tracker-server` from source
|
||||
2. Installs it via Homebrew
|
||||
3. Restarts the brew service
|
||||
|
|
@ -603,7 +603,7 @@ func runUI(args []string) error {
|
|||
ic, hasIC := parseTimestamp(notes[i].CreatedAt)
|
||||
jc, hasJC := parseTimestamp(notes[j].CreatedAt)
|
||||
if hasIC && hasJC && !ic.Equal(jc) {
|
||||
return ic.After(jc)
|
||||
return ic.Before(jc)
|
||||
}
|
||||
if hasIC != hasJC {
|
||||
return hasIC
|
||||
|
|
|
|||
|
|
@ -425,6 +425,9 @@ func (s *server) handleCommand(env ipc.Envelope) error {
|
|||
}
|
||||
|
||||
func (s *server) startTask(target tmuxTarget, summary string) error {
|
||||
if target.SessionID == "" || target.WindowID == "" {
|
||||
return fmt.Errorf("cannot create task: missing session or window ID")
|
||||
}
|
||||
now := time.Now()
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
@ -441,6 +444,9 @@ func (s *server) startTask(target tmuxTarget, summary string) error {
|
|||
}
|
||||
|
||||
func (s *server) finishTask(target tmuxTarget, note string) error {
|
||||
if target.SessionID == "" || target.WindowID == "" {
|
||||
return nil // silently ignore - pane likely died
|
||||
}
|
||||
now := time.Now()
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
@ -458,7 +464,8 @@ func (s *server) finishTask(target tmuxTarget, note string) error {
|
|||
if note != "" {
|
||||
t.CompletionNote = note
|
||||
}
|
||||
t.Acknowledged = false
|
||||
// Auto-acknowledge if user is currently in this pane
|
||||
t.Acknowledged = isActivePane(target.PaneID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -1217,16 +1224,19 @@ func (s *server) buildStateEnvelope() *ipc.Envelope {
|
|||
visible := s.visible
|
||||
pos := s.pos
|
||||
copies := make([]*taskRecord, 0, len(s.tasks))
|
||||
for _, task := range s.tasks {
|
||||
taskKeys := make([]string, 0, len(s.tasks))
|
||||
for key, task := range s.tasks {
|
||||
copy := *task
|
||||
copies = append(copies, ©)
|
||||
taskKeys = append(taskKeys, key)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
tasks := make([]ipc.Task, 0, len(copies))
|
||||
var staleKeys []string
|
||||
nameCache := make(map[string][2]string)
|
||||
for _, t := range copies {
|
||||
for i, t := range copies {
|
||||
started := ""
|
||||
if !t.StartedAt.IsZero() {
|
||||
started = t.StartedAt.Format(time.RFC3339)
|
||||
|
|
@ -1242,14 +1252,24 @@ func (s *server) buildStateEnvelope() *ipc.Envelope {
|
|||
if duration < 0 {
|
||||
duration = 0
|
||||
}
|
||||
// Auto-timeout: in_progress tasks older than 30 minutes
|
||||
if t.Status == statusInProgress && duration > 30*time.Minute {
|
||||
staleKeys = append(staleKeys, taskKeys[i])
|
||||
continue
|
||||
}
|
||||
var names [2]string
|
||||
if cached, ok := nameCache[t.WindowID]; ok {
|
||||
if cached[0] == "" && cached[1] == "" {
|
||||
staleKeys = append(staleKeys, taskKeys[i])
|
||||
continue
|
||||
}
|
||||
names = cached
|
||||
} else {
|
||||
sessName, winName, err := tmuxNamesForWindow(t.WindowID)
|
||||
if err != nil {
|
||||
sessName = t.SessionID
|
||||
winName = t.WindowID
|
||||
if err != nil || (sessName == "" && winName == "") {
|
||||
nameCache[t.WindowID] = [2]string{"", ""}
|
||||
staleKeys = append(staleKeys, taskKeys[i])
|
||||
continue
|
||||
}
|
||||
names = [2]string{sessName, winName}
|
||||
nameCache[t.WindowID] = names
|
||||
|
|
@ -1271,6 +1291,15 @@ func (s *server) buildStateEnvelope() *ipc.Envelope {
|
|||
})
|
||||
}
|
||||
|
||||
// Clean up stale tasks (windows that no longer exist)
|
||||
if len(staleKeys) > 0 {
|
||||
s.mu.Lock()
|
||||
for _, key := range staleKeys {
|
||||
delete(s.tasks, key)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
activeNotes, archived := s.notesForState()
|
||||
|
||||
s.mu.Lock()
|
||||
|
|
@ -1406,6 +1435,23 @@ func runTmux(args ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func isActivePane(paneID string) bool {
|
||||
clients, err := listClients()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, client := range clients {
|
||||
output, err := tmuxDisplay(client, "#{pane_id}")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(output) == paneID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tmuxOutput(args ...string) (string, error) {
|
||||
cmd := exec.Command("tmux", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@ fi
|
|||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SERVER_BIN="$ROOT_DIR/bin/tracker-server"
|
||||
|
||||
if [[ ! -x "$SERVER_BIN" ]]; then
|
||||
echo "Error: tracker-server binary not found at $SERVER_BIN" >&2
|
||||
echo "Build it with: (cd $ROOT_DIR && ./install.sh)" >&2
|
||||
echo "Building tracker-server..." >&2
|
||||
mkdir -p "$ROOT_DIR/bin"
|
||||
if ! (cd "$ROOT_DIR" && go build -o bin/tracker-server ./cmd/tracker-server); then
|
||||
echo "Error: go build failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -22,12 +22,6 @@ CRITICAL WORKFLOW REQUIREMENT
|
|||
- Avoid getting stuck. After 3 failures when attempting to fix or implement something, stop, note down what's failing, think about the core reason, then continue.
|
||||
- When migrating or refactoring code, do not leave legacy code. Remove all deprecated or unused code.
|
||||
|
||||
Other recommendations:
|
||||
- When giving the user bullet lists, use different bullet characters for different levels
|
||||
- Use numbered lists for options/confirmations.
|
||||
- Prompt users to reply compactly (e.g., "1Y 2N 3Y").
|
||||
- Default to numbers for multi-step plans and checklists.
|
||||
|
||||
---------
|
||||
|
||||
## Code Change Guidelines
|
||||
|
|
|
|||
|
|
@ -4,14 +4,59 @@ const MAX_SUMMARY_CHARS = 600;
|
|||
const TRACKER_BIN = "/Users/david/.config/agent-tracker/bin/tracker-client";
|
||||
|
||||
export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
||||
// Only run within tmux (TMUX_PANE must be set)
|
||||
const TMUX_PANE = process.env.TMUX_PANE;
|
||||
if (!TMUX_PANE) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Resolve tmux context once at startup to avoid race conditions
|
||||
let tmuxContext = null;
|
||||
const resolveTmuxContext = async () => {
|
||||
if (tmuxContext) return tmuxContext;
|
||||
try {
|
||||
const result = await $`tmux display-message -p -t ${TMUX_PANE} "#{session_id}:::#{window_id}:::#{pane_id}"`.quiet();
|
||||
const parts = result.stdout.trim().split(":::");
|
||||
if (parts.length === 3) {
|
||||
tmuxContext = {
|
||||
sessionId: parts[0],
|
||||
windowId: parts[1],
|
||||
paneId: parts[2],
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fallback: use TMUX_PANE directly
|
||||
tmuxContext = { paneId: TMUX_PANE };
|
||||
}
|
||||
return tmuxContext;
|
||||
};
|
||||
await resolveTmuxContext();
|
||||
|
||||
let taskActive = false;
|
||||
let currentSessionID = null;
|
||||
let lastUserMessage = "";
|
||||
|
||||
const trackerReady = async () => {
|
||||
const check = await $`test -x ${TRACKER_BIN}`.nothrow();
|
||||
return check?.exitCode === 0;
|
||||
};
|
||||
|
||||
const buildTrackerArgs = () => {
|
||||
const args = [];
|
||||
if (tmuxContext?.sessionId) args.push("-session-id", tmuxContext.sessionId);
|
||||
if (tmuxContext?.windowId) args.push("-window-id", tmuxContext.windowId);
|
||||
if (tmuxContext?.paneId) args.push("-pane", tmuxContext.paneId);
|
||||
return args;
|
||||
};
|
||||
|
||||
// On init: finish any stale task for this pane
|
||||
const finishStaleTask = async () => {
|
||||
if (!(await trackerReady())) return;
|
||||
const args = buildTrackerArgs();
|
||||
await $`${TRACKER_BIN} command ${args} -summary "stale" finish_task`.nothrow();
|
||||
};
|
||||
finishStaleTask();
|
||||
|
||||
const summarizeText = (parts = []) => {
|
||||
const text = parts
|
||||
.filter((p) => p?.type === "text" && !p.ignored)
|
||||
|
|
@ -34,7 +79,8 @@ export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
|||
if (!(await trackerReady())) return;
|
||||
taskActive = true;
|
||||
currentSessionID = sessionID;
|
||||
await $`${TRACKER_BIN} command -summary ${summary} start_task`.nothrow();
|
||||
const args = buildTrackerArgs();
|
||||
await $`${TRACKER_BIN} command ${args} -summary ${summary} start_task`.nothrow();
|
||||
};
|
||||
|
||||
const finishTask = async (summary) => {
|
||||
|
|
@ -42,7 +88,8 @@ export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
|||
if (!(await trackerReady())) return;
|
||||
taskActive = false;
|
||||
currentSessionID = null;
|
||||
await $`${TRACKER_BIN} command -summary ${summary || "done"} finish_task`.nothrow();
|
||||
const args = buildTrackerArgs();
|
||||
await $`${TRACKER_BIN} command ${args} -summary ${summary || "done"} finish_task`.nothrow();
|
||||
};
|
||||
|
||||
const notify = async (sessionID) => {
|
||||
|
|
@ -78,87 +125,79 @@ export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const isSessionTrulyIdle = async (sessionID) => {
|
||||
try {
|
||||
const messages =
|
||||
(await client.session.messages({
|
||||
path: { id: sessionID },
|
||||
query: { directory },
|
||||
})) || [];
|
||||
if (!messages.length) return true;
|
||||
|
||||
const last = messages[messages.length - 1];
|
||||
if (!last?.info) return true;
|
||||
|
||||
// If last message is from user, assistant hasn't responded yet
|
||||
if (last.info.role === "user") return false;
|
||||
|
||||
// If last message is assistant, check if it has completed
|
||||
return !!last.info.time?.completed;
|
||||
} catch {
|
||||
return true;
|
||||
const getLastMessageText = async (sessionID, role, retries = 3) => {
|
||||
for (let attempt = 0; attempt < retries; attempt++) {
|
||||
try {
|
||||
const messages =
|
||||
(await client.session.messages({
|
||||
path: { id: sessionID },
|
||||
query: { directory },
|
||||
})) || [];
|
||||
const msg = [...messages]
|
||||
.reverse()
|
||||
.find((m) => m?.info?.role === role);
|
||||
if (msg) {
|
||||
const text = summarizeText(msg.parts);
|
||||
if (text) return text;
|
||||
}
|
||||
} catch {
|
||||
// ignore fetch errors
|
||||
}
|
||||
if (attempt < retries - 1) {
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const tryFinishTask = async (sessionID) => {
|
||||
if (!taskActive) return;
|
||||
// Only finish for the session that started the task
|
||||
if (currentSessionID && sessionID !== currentSessionID) return;
|
||||
|
||||
// Verify session is truly idle (assistant message completed)
|
||||
if (!(await isSessionTrulyIdle(sessionID))) return;
|
||||
|
||||
let text = "";
|
||||
try {
|
||||
const messages =
|
||||
(await client.session.messages({
|
||||
path: { id: sessionID },
|
||||
query: { directory },
|
||||
})) || [];
|
||||
const assistant = [...messages]
|
||||
.reverse()
|
||||
.find((m) => m?.info?.role === "assistant");
|
||||
if (assistant) text = summarizeText(assistant.parts);
|
||||
} catch {
|
||||
// ignore fetch errors
|
||||
}
|
||||
await finishTask(text || "done");
|
||||
await notify(sessionID);
|
||||
};
|
||||
// Track message IDs to their roles
|
||||
const messageRoles = new Map();
|
||||
|
||||
return {
|
||||
event: async ({ event }) => {
|
||||
// session.idle event - verify with message state
|
||||
if (event?.type === "session.idle" && event?.properties?.sessionID) {
|
||||
await tryFinishTask(event.properties.sessionID);
|
||||
return;
|
||||
}
|
||||
|
||||
// session.status event - "idle" status is more reliable
|
||||
if (event?.type === "session.status") {
|
||||
const sessionID = event?.properties?.sessionID;
|
||||
const status = event?.properties?.status;
|
||||
if (sessionID && status === "idle") {
|
||||
await tryFinishTask(sessionID);
|
||||
// Track message roles from message.updated events
|
||||
if (event?.type === "message.updated") {
|
||||
const info = event?.properties?.info;
|
||||
if (info?.id && info?.role) {
|
||||
messageRoles.set(info.id, info.role);
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
"chat.message": async (_input, output) => {
|
||||
if (output?.message?.role !== "user") return;
|
||||
const sessionID = output?.message?.sessionID;
|
||||
const summary = summarizeText(output.parts).slice(0, 200);
|
||||
await startTask(summary, sessionID);
|
||||
},
|
||||
"message.updated": async ({ event }) => {
|
||||
// When an assistant message is updated with time.completed, the turn is done
|
||||
if (event?.properties?.info?.role !== "assistant") return;
|
||||
if (!event?.properties?.info?.time?.completed) return;
|
||||
|
||||
const sessionID = event?.properties?.info?.sessionID;
|
||||
if (!sessionID) return;
|
||||
// Capture user message text from message.part.updated
|
||||
if (event?.type === "message.part.updated") {
|
||||
const part = event?.properties?.part;
|
||||
if (part?.type === "text" && part?.text && part?.messageID) {
|
||||
const role = messageRoles.get(part.messageID);
|
||||
// Capture if it's a user message, or if we're not yet in a task (user input comes first)
|
||||
if (role === "user" || (!role && !taskActive)) {
|
||||
const text = part.text?.trim();
|
||||
if (text && text.length > 0) {
|
||||
lastUserMessage = text.slice(0, MAX_SUMMARY_CHARS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await tryFinishTask(sessionID);
|
||||
if (event?.type !== "session.status") return;
|
||||
|
||||
const sessionID = event?.properties?.sessionID;
|
||||
const status = event?.properties?.status;
|
||||
if (!sessionID || !status) return;
|
||||
|
||||
if (status.type === "busy" && !taskActive) {
|
||||
// Use captured message first, then fall back to API
|
||||
let text = lastUserMessage;
|
||||
if (!text) {
|
||||
text = await getLastMessageText(sessionID, "user");
|
||||
}
|
||||
await startTask(text || "working...", sessionID);
|
||||
lastUserMessage = "";
|
||||
} else if (status.type === "idle" && taskActive) {
|
||||
if (currentSessionID && sessionID !== currentSessionID) return;
|
||||
const text = await getLastMessageText(sessionID, "assistant");
|
||||
await finishTask(text || "done");
|
||||
await notify(sessionID);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -57,6 +57,32 @@ if [[ -z "$sessions" ]]; then
|
|||
exit 0
|
||||
fi
|
||||
|
||||
# Refresh tracker cache (fast - skips if recent)
|
||||
"$HOME/.config/tmux/tmux-status/tracker_cache.sh" 2>/dev/null || true
|
||||
|
||||
# Read from cache
|
||||
CACHE_FILE="/tmp/tmux-tracker-cache.json"
|
||||
tracker_state=""
|
||||
if [[ -f "$CACHE_FILE" ]]; then
|
||||
tracker_state=$(cat "$CACHE_FILE" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
get_session_icon() {
|
||||
local sid="$1"
|
||||
[[ -z "$tracker_state" ]] && return
|
||||
local result
|
||||
result=$(echo "$tracker_state" | jq -r --arg sid "$sid" '
|
||||
.tasks // [] | .[] | select(.session_id == $sid) |
|
||||
if .status == "in_progress" then "in_progress"
|
||||
elif .status == "completed" and .acknowledged != true then "waiting"
|
||||
else empty end
|
||||
' 2>/dev/null | head -1 || true)
|
||||
case "$result" in
|
||||
in_progress) printf '⏳' ;;
|
||||
waiting) printf '🔔' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
rendered=""
|
||||
prev_bg=""
|
||||
current_session_id_norm=$(normalize_session_id "$current_session_id")
|
||||
|
|
@ -96,12 +122,14 @@ while IFS= read -r entry; do
|
|||
label="${label:0:max_width-1}…"
|
||||
fi
|
||||
|
||||
task_icon=$(get_session_icon "$session_id")
|
||||
|
||||
if [[ -z "$prev_bg" ]]; then
|
||||
rendered+="#[fg=${segment_bg},bg=${status_bg}]${left_cap}"
|
||||
else
|
||||
rendered+="#[fg=${prev_bg},bg=${segment_bg}]${separator}"
|
||||
fi
|
||||
rendered+="#[fg=${segment_fg},bg=${segment_bg}] ${label} "
|
||||
rendered+="#[fg=${segment_fg},bg=${segment_bg}] ${label}${task_icon} "
|
||||
prev_bg="$segment_bg"
|
||||
done <<< "$sessions"
|
||||
|
||||
|
|
|
|||
24
tmux/tmux-status/notes_count.sh
Executable file
24
tmux/tmux-status/notes_count.sh
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
window_id=$(tmux display-message -p '#{window_id}' 2>/dev/null || true)
|
||||
[[ -z "$window_id" ]] && exit 0
|
||||
|
||||
CACHE_FILE="/tmp/tmux-tracker-cache.json"
|
||||
[[ ! -f "$CACHE_FILE" ]] && exit 0
|
||||
|
||||
state=$(cat "$CACHE_FILE" 2>/dev/null || true)
|
||||
[[ -z "$state" ]] && exit 0
|
||||
|
||||
count=$(echo "$state" | jq -r --arg wid "$window_id" '
|
||||
[.notes // [] | .[] | select(
|
||||
.archived != true and
|
||||
.completed != true and
|
||||
.scope == "window" and
|
||||
.window_id == $wid
|
||||
)] | length
|
||||
' 2>/dev/null || echo "0")
|
||||
|
||||
if [[ "$count" =~ ^[0-9]+$ ]] && (( count > 0 )); then
|
||||
printf ' %s ' "$count"
|
||||
fi
|
||||
|
|
@ -52,17 +52,38 @@ if [[ "$rainbarf_toggle" == "1" ]] && command -v rainbarf >/dev/null 2>&1; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# Notes count for current window (red if > 0, hidden otherwise)
|
||||
notes_segment=""
|
||||
notes_count_script="$HOME/.config/tmux/tmux-status/notes_count.sh"
|
||||
if [[ -x "$notes_count_script" ]]; then
|
||||
notes_output=$("$notes_count_script" 2>/dev/null || true)
|
||||
if [[ -n "$notes_output" ]]; then
|
||||
notes_connector_bg="$status_bg"
|
||||
if [[ -n "$rainbarf_segment" ]]; then
|
||||
notes_connector_bg="$rainbarf_bg"
|
||||
fi
|
||||
notes_bg="#cc6666"
|
||||
notes_fg="#1d1f21"
|
||||
notes_segment=$(printf '#[fg=%s,bg=%s]%s#[fg=%s,bg=%s,bold]%s#[default]' \
|
||||
"$notes_bg" "$notes_connector_bg" "$separator" \
|
||||
"$notes_fg" "$notes_bg" "$notes_output")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build a connector into the hostname segment using host colors
|
||||
host_connector_bg="$status_bg"
|
||||
if [[ -n "$rainbarf_segment" ]]; then
|
||||
if [[ -n "$notes_segment" ]]; then
|
||||
host_connector_bg="#cc6666"
|
||||
elif [[ -n "$rainbarf_segment" ]]; then
|
||||
host_connector_bg="$rainbarf_bg"
|
||||
fi
|
||||
host_prefix=$(printf '#[fg=%s,bg=%s]%s#[fg=%s,bg=%s] ' \
|
||||
"$host_bg" "$host_connector_bg" "$separator" \
|
||||
"$host_fg" "$host_bg")
|
||||
|
||||
printf '%s%s%s #[fg=%s,bg=%s]%s' \
|
||||
printf '%s%s%s%s #[fg=%s,bg=%s]%s' \
|
||||
"$rainbarf_segment" \
|
||||
"$notes_segment" \
|
||||
"$host_prefix" \
|
||||
"$hostname" \
|
||||
"$host_bg" "$status_bg" "$right_cap"
|
||||
|
|
|
|||
24
tmux/tmux-status/session_task_icon.sh
Executable file
24
tmux/tmux-status/session_task_icon.sh
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
session_id="$1"
|
||||
[[ -z "$session_id" ]] && exit 0
|
||||
|
||||
tracker_client="$HOME/.config/agent-tracker/bin/tracker-client"
|
||||
[[ ! -x "$tracker_client" ]] && exit 0
|
||||
|
||||
state=$("$tracker_client" state 2>/dev/null || true)
|
||||
[[ -z "$state" ]] && exit 0
|
||||
|
||||
# Check for in_progress or unacknowledged completed tasks in this session
|
||||
result=$(echo "$state" | jq -r --arg sid "$session_id" '
|
||||
.tasks // [] | .[] | select(.session_id == $sid) |
|
||||
if .status == "in_progress" then "in_progress"
|
||||
elif .status == "completed" and .acknowledged != true then "waiting"
|
||||
else empty end
|
||||
' 2>/dev/null | head -1 || true)
|
||||
|
||||
case "$result" in
|
||||
in_progress) printf '⏳' ;;
|
||||
waiting) printf '🔔' ;;
|
||||
esac
|
||||
32
tmux/tmux-status/tracker_cache.sh
Executable file
32
tmux/tmux-status/tracker_cache.sh
Executable file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CACHE_FILE="/tmp/tmux-tracker-cache.json"
|
||||
CACHE_MAX_AGE=1
|
||||
|
||||
tracker_client="$HOME/.config/agent-tracker/bin/tracker-client"
|
||||
|
||||
# Check if cache is fresh enough
|
||||
if [[ -f "$CACHE_FILE" ]]; then
|
||||
if [[ "$OSTYPE" == darwin* ]]; then
|
||||
file_age=$(( $(date +%s) - $(stat -f %m "$CACHE_FILE") ))
|
||||
else
|
||||
file_age=$(( $(date +%s) - $(stat -c %Y "$CACHE_FILE") ))
|
||||
fi
|
||||
if (( file_age < CACHE_MAX_AGE )); then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Simple lock using mkdir (atomic on all systems)
|
||||
LOCK_DIR="/tmp/tmux-tracker-cache.lock"
|
||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT
|
||||
|
||||
if [[ -x "$tracker_client" ]]; then
|
||||
"$tracker_client" state 2>/dev/null > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE"
|
||||
else
|
||||
echo '{}' > "$CACHE_FILE"
|
||||
fi
|
||||
23
tmux/tmux-status/window_task_icon.sh
Executable file
23
tmux/tmux-status/window_task_icon.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
window_id="$1"
|
||||
[[ -z "$window_id" ]] && exit 0
|
||||
|
||||
CACHE_FILE="/tmp/tmux-tracker-cache.json"
|
||||
[[ ! -f "$CACHE_FILE" ]] && exit 0
|
||||
|
||||
state=$(cat "$CACHE_FILE" 2>/dev/null || true)
|
||||
[[ -z "$state" ]] && exit 0
|
||||
|
||||
result=$(echo "$state" | jq -r --arg wid "$window_id" '
|
||||
.tasks // [] | .[] | select(.window_id == $wid) |
|
||||
if .status == "in_progress" then "in_progress"
|
||||
elif .status == "completed" and .acknowledged != true then "waiting"
|
||||
else empty end
|
||||
' 2>/dev/null | head -1 || true)
|
||||
|
||||
case "$result" in
|
||||
in_progress) printf '⏳' ;;
|
||||
waiting) printf '🔔' ;;
|
||||
esac
|
||||
Loading…
Add table
Add a link
Reference in a new issue