mirror of
https://github.com/theniceboy/.config.git
synced 2026-07-16 22:01:21 +08:00
agent tracker + tmux + opencode updates
This commit is contained in:
parent
75403c927f
commit
c56de89d3d
6 changed files with 117 additions and 122 deletions
|
|
@ -6,9 +6,9 @@
|
||||||
|
|
||||||
## Work Summary
|
## Work Summary
|
||||||
|
|
||||||
- `set_work_summary` is mandatory protocol, not a suggestion.
|
- Keep `set_work_summary` up to date during the turn.
|
||||||
- It is fine to inspect first. You may use exploratory tools like search, read, task, web, or read-only shell commands before calling `set_work_summary`.
|
- You may call any tools before `set_work_summary` when you need more context.
|
||||||
- Call `set_work_summary` once you have enough context to label the work accurately, and always before edits, patches, or other committed actions.
|
- Call `set_work_summary` once you understand the work clearly enough to label it well, and update it again when the focus materially changes.
|
||||||
- Prefer calling it with both fields: `set_work_summary({ theme: "...", now: "..." })`.
|
- Prefer calling it with both fields: `set_work_summary({ theme: "...", now: "..." })`.
|
||||||
- `theme` answers: what is this pane about overall? Keep it stable across many turns.
|
- `theme` answers: what is this pane about overall? Keep it stable across many turns.
|
||||||
- `now` answers: what are you about to do next? Update it whenever the next concrete step changes.
|
- `now` answers: what are you about to do next? Update it whenever the next concrete step changes.
|
||||||
|
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
import type { Plugin } from "@opencode-ai/plugin"
|
|
||||||
|
|
||||||
type TurnState = {
|
|
||||||
summaryCalled: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const states = new Map<string, TurnState>()
|
|
||||||
const exploratoryTools = new Set([
|
|
||||||
"bash",
|
|
||||||
"read",
|
|
||||||
"glob",
|
|
||||||
"grep",
|
|
||||||
"task",
|
|
||||||
"question",
|
|
||||||
"webfetch",
|
|
||||||
"web_google_search",
|
|
||||||
"web_website_fetch",
|
|
||||||
"web_website_search",
|
|
||||||
"web_website_outline",
|
|
||||||
"web_website_extract_section",
|
|
||||||
"web_website_extract_pricing",
|
|
||||||
])
|
|
||||||
|
|
||||||
function sessionState(sessionID: string) {
|
|
||||||
let state = states.get(sessionID)
|
|
||||||
if (!state) {
|
|
||||||
state = { summaryCalled: false }
|
|
||||||
states.set(sessionID, state)
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
function eventSessionID(event: any) {
|
|
||||||
return String(
|
|
||||||
event?.properties?.sessionID ||
|
|
||||||
event?.properties?.session?.id ||
|
|
||||||
event?.properties?.info?.sessionID ||
|
|
||||||
"",
|
|
||||||
).trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
function isExploratoryTool(tool: string) {
|
|
||||||
return exploratoryTools.has(tool)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RequireWorkSummaryPlugin: Plugin = async () => {
|
|
||||||
return {
|
|
||||||
"chat.message": async (input: any) => {
|
|
||||||
const sessionID = String(input?.sessionID || "").trim()
|
|
||||||
if (!sessionID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sessionState(sessionID).summaryCalled = false
|
|
||||||
},
|
|
||||||
|
|
||||||
"tool.execute.before": async (input: any) => {
|
|
||||||
const sessionID = String(input?.sessionID || "").trim()
|
|
||||||
if (!sessionID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const tool = String(input?.tool || "").trim()
|
|
||||||
if (!tool) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = sessionState(sessionID)
|
|
||||||
if (tool === "set_work_summary") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isExploratoryTool(tool)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.summaryCalled) {
|
|
||||||
throw new Error(
|
|
||||||
"Exploration tools can run first, but call set_work_summary before edits or other committed actions.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"tool.execute.after": async (input: any) => {
|
|
||||||
const sessionID = String(input?.sessionID || "").trim()
|
|
||||||
if (!sessionID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const tool = String(input?.tool || "").trim()
|
|
||||||
if (tool === "set_work_summary") {
|
|
||||||
sessionState(sessionID).summaryCalled = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
event: async ({ event }) => {
|
|
||||||
if (event?.type === "session.deleted") {
|
|
||||||
const sessionID = eventSessionID(event)
|
|
||||||
if (sessionID) {
|
|
||||||
states.delete(sessionID)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RequireWorkSummaryPlugin
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { appendFileSync, mkdirSync, renameSync, writeFileSync } from "fs";
|
import { appendFileSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
||||||
|
|
||||||
const NOTIFY_BIN = "/usr/bin/python3";
|
const NOTIFY_BIN = "/usr/bin/python3";
|
||||||
const NOTIFY_SCRIPT = "/Users/david/.config/codex/notify.py";
|
const NOTIFY_SCRIPT = "/Users/david/.config/codex/notify.py";
|
||||||
|
|
@ -8,6 +8,7 @@ const LOG_FILE = "/tmp/tracker-notify-debug.log";
|
||||||
const STATE_ROOT = process.env.XDG_STATE_HOME || `${process.env.HOME || ""}/.local/state`;
|
const STATE_ROOT = process.env.XDG_STATE_HOME || `${process.env.HOME || ""}/.local/state`;
|
||||||
const OP_STATE_DIR = `${STATE_ROOT}/op`;
|
const OP_STATE_DIR = `${STATE_ROOT}/op`;
|
||||||
const TMUX_BINS = [process.env.TMUX_BIN, "/opt/homebrew/bin/tmux", "tmux"].filter(Boolean);
|
const TMUX_BINS = [process.env.TMUX_BIN, "/opt/homebrew/bin/tmux", "tmux"].filter(Boolean);
|
||||||
|
const TMUX_QUESTION_OPTION = "@op_question_pending";
|
||||||
|
|
||||||
const log = (msg: string, data?: any) => {
|
const log = (msg: string, data?: any) => {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
|
|
@ -70,17 +71,30 @@ export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
||||||
}
|
}
|
||||||
return `${tmuxContext.sessionName}:${tmuxContext.windowIndex}.${tmuxContext.paneIndex}`;
|
return `${tmuxContext.sessionName}:${tmuxContext.windowIndex}.${tmuxContext.paneIndex}`;
|
||||||
};
|
};
|
||||||
const persistPaneSessionMap = async (sessionID) => {
|
const paneSessionStateFile = () => {
|
||||||
const locator = paneLocator();
|
const locator = paneLocator();
|
||||||
if (!sessionID || !locator) return;
|
if (!locator) return "";
|
||||||
const stateFile = `${OP_STATE_DIR}/loc_${sanitizeKey(locator)}`;
|
return `${OP_STATE_DIR}/loc_${sanitizeKey(locator)}`;
|
||||||
|
};
|
||||||
|
const persistPaneSessionMap = async (sessionID) => {
|
||||||
|
const stateFile = paneSessionStateFile();
|
||||||
|
if (!sessionID || !stateFile) return;
|
||||||
try {
|
try {
|
||||||
mkdirSync(OP_STATE_DIR, { recursive: true });
|
mkdirSync(OP_STATE_DIR, { recursive: true });
|
||||||
const tmpPath = `${stateFile}.tmp`;
|
const tmpPath = `${stateFile}.tmp`;
|
||||||
writeFileSync(tmpPath, `${sessionID}\n`, "utf8");
|
writeFileSync(tmpPath, `${sessionID}\n`, "utf8");
|
||||||
renameSync(tmpPath, stateFile);
|
renameSync(tmpPath, stateFile);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log("failed to persist pane session", { locator, error: String(error) });
|
log("failed to persist pane session", { stateFile, error: String(error) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const loadPersistedPaneSessionID = () => {
|
||||||
|
const stateFile = paneSessionStateFile();
|
||||||
|
if (!stateFile) return "";
|
||||||
|
try {
|
||||||
|
return readFileSync(stateFile, "utf8").trim();
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const eventSessionID = (event) => {
|
const eventSessionID = (event) => {
|
||||||
|
|
@ -95,6 +109,60 @@ export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
||||||
let taskActive = false;
|
let taskActive = false;
|
||||||
let currentSessionID = null;
|
let currentSessionID = null;
|
||||||
let lastUserMessage = "";
|
let lastUserMessage = "";
|
||||||
|
let rootSessionID = loadPersistedPaneSessionID();
|
||||||
|
let questionPending: boolean | null = null;
|
||||||
|
|
||||||
|
const setTmuxPaneOption = async (option, value: string | null) => {
|
||||||
|
if (!tmuxContext?.paneId) return;
|
||||||
|
for (const tmuxBin of TMUX_BINS) {
|
||||||
|
const cmd =
|
||||||
|
value === null
|
||||||
|
? await $`${tmuxBin} set-option -p -u -t ${tmuxContext.paneId} ${option}`.nothrow()
|
||||||
|
: await $`${tmuxBin} set-option -p -t ${tmuxContext.paneId} ${option} ${value}`.nothrow();
|
||||||
|
if (cmd?.exitCode === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyQuestionPending = async (pending: boolean) => {
|
||||||
|
if (questionPending === pending) return;
|
||||||
|
questionPending = pending;
|
||||||
|
await setTmuxPaneOption(TMUX_QUESTION_OPTION, pending ? "1" : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const listPendingQuestions = async () => {
|
||||||
|
try {
|
||||||
|
const response = await client.question.list({ directory });
|
||||||
|
if (Array.isArray(response?.data)) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncPendingQuestionState = async (sessionID = rootSessionID) => {
|
||||||
|
const effectiveSessionID = sessionID || loadPersistedPaneSessionID();
|
||||||
|
if (!effectiveSessionID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rootSessionID = effectiveSessionID;
|
||||||
|
const pending = (await listPendingQuestions()).some(
|
||||||
|
(question) => question?.sessionID === effectiveSessionID,
|
||||||
|
);
|
||||||
|
await applyQuestionPending(pending);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rootSessionID) {
|
||||||
|
await syncPendingQuestionState(rootSessionID);
|
||||||
|
} else {
|
||||||
|
await applyQuestionPending(false);
|
||||||
|
}
|
||||||
|
|
||||||
const trackerReady = async () => {
|
const trackerReady = async () => {
|
||||||
const check = await $`test -x ${TRACKER_BIN}`.nothrow();
|
const check = await $`test -x ${TRACKER_BIN}`.nothrow();
|
||||||
|
|
@ -208,6 +276,10 @@ export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
||||||
return {
|
return {
|
||||||
"tool.execute.before": async (input, output) => {
|
"tool.execute.before": async (input, output) => {
|
||||||
if (input.tool === "question") {
|
if (input.tool === "question") {
|
||||||
|
if (!rootSessionID && input?.sessionID) {
|
||||||
|
rootSessionID = input.sessionID;
|
||||||
|
}
|
||||||
|
await applyQuestionPending(true);
|
||||||
log("Question tool called:", {
|
log("Question tool called:", {
|
||||||
questions: output.args?.questions || "no questions",
|
questions: output.args?.questions || "no questions",
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
|
|
@ -216,6 +288,16 @@ export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
event: async ({ event }) => {
|
event: async ({ event }) => {
|
||||||
|
if (event?.type === "question.asked") {
|
||||||
|
await applyQuestionPending(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event?.type === "question.replied" || event?.type === "question.rejected") {
|
||||||
|
const sessionID = event?.properties?.sessionID || rootSessionID;
|
||||||
|
await syncPendingQuestionState(sessionID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Track message roles from message.updated events
|
// Track message roles from message.updated events
|
||||||
if (event?.type === "message.updated") {
|
if (event?.type === "message.updated") {
|
||||||
|
|
@ -254,7 +336,12 @@ export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sessionChanged = sessionID !== rootSessionID;
|
||||||
|
rootSessionID = sessionID;
|
||||||
await persistPaneSessionMap(sessionID);
|
await persistPaneSessionMap(sessionID);
|
||||||
|
if (sessionChanged) {
|
||||||
|
await syncPendingQuestionState(sessionID);
|
||||||
|
}
|
||||||
if (event?.type !== "session.status") return;
|
if (event?.type !== "session.status") return;
|
||||||
|
|
||||||
const status = event?.properties?.status;
|
const status = event?.properties?.status;
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,12 @@ load_option() {
|
||||||
printf '%s' "$value"
|
printf '%s' "$value"
|
||||||
}
|
}
|
||||||
|
|
||||||
clear_work_labels() {
|
clear_opencode_state() {
|
||||||
[[ -n "$pane_id" ]] || return 0
|
[[ -n "$pane_id" ]] || return 0
|
||||||
tmux set-option -p -u -t "$pane_id" @op_work_theme 2>/dev/null || true
|
tmux set-option -p -u -t "$pane_id" @op_work_theme 2>/dev/null || true
|
||||||
tmux set-option -p -u -t "$pane_id" @op_work_now 2>/dev/null || true
|
tmux set-option -p -u -t "$pane_id" @op_work_now 2>/dev/null || true
|
||||||
tmux set-option -p -u -t "$pane_id" @op_work_summary 2>/dev/null || true
|
tmux set-option -p -u -t "$pane_id" @op_work_summary 2>/dev/null || true
|
||||||
|
tmux set-option -p -u -t "$pane_id" @op_question_pending 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
opencode_active() {
|
opencode_active() {
|
||||||
|
|
@ -102,10 +103,11 @@ if [[ -z "$theme" ]]; then
|
||||||
theme=$(load_option "@op_work_summary")
|
theme=$(load_option "@op_work_summary")
|
||||||
fi
|
fi
|
||||||
now=$(load_option "@op_work_now")
|
now=$(load_option "@op_work_now")
|
||||||
|
question_pending=$(load_option "@op_question_pending")
|
||||||
|
|
||||||
if ! opencode_active; then
|
if ! opencode_active; then
|
||||||
if [[ -n "$theme" || -n "$now" ]]; then
|
if [[ -n "$theme" || -n "$now" || -n "$question_pending" ]]; then
|
||||||
clear_work_labels
|
clear_opencode_state
|
||||||
fi
|
fi
|
||||||
printf '%s' "$title"
|
printf '%s' "$title"
|
||||||
exit 0
|
exit 0
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,15 @@ if [[ -f "$CACHE_FILE" ]]; then
|
||||||
tracker_state=$(cat "$CACHE_FILE" 2>/dev/null || true)
|
tracker_state=$(cat "$CACHE_FILE" 2>/dev/null || true)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
question_state=$(tmux list-panes -a -F '#{session_id}::#{@op_question_pending}' 2>/dev/null || true)
|
||||||
|
|
||||||
get_session_icon() {
|
get_session_icon() {
|
||||||
local sid="$1"
|
local sid="$1"
|
||||||
local has_bell=0 has_watch=0
|
local has_question=0 has_bell=0 has_watch=0
|
||||||
|
|
||||||
|
local question_pane
|
||||||
|
question_pane=$(grep -F -m1 -x "${sid}::1" <<< "$question_state" || true)
|
||||||
|
[[ -n "$question_pane" ]] && has_question=1
|
||||||
|
|
||||||
local unread_win
|
local unread_win
|
||||||
unread_win=$(tmux list-windows -t "$sid" -F '#{@unread}' 2>/dev/null | grep -m1 '^1$' || true)
|
unread_win=$(tmux list-windows -t "$sid" -F '#{@unread}' 2>/dev/null | grep -m1 '^1$' || true)
|
||||||
|
|
@ -87,7 +93,9 @@ get_session_icon() {
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if (( has_bell )); then
|
if (( has_question )); then
|
||||||
|
printf '❓'
|
||||||
|
elif (( has_bell )); then
|
||||||
printf '🔔'
|
printf '🔔'
|
||||||
elif (( has_watch )); then
|
elif (( has_watch )); then
|
||||||
printf '⏳'
|
printf '⏳'
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,15 @@ watching="${3:-0}"
|
||||||
|
|
||||||
has_bell=0
|
has_bell=0
|
||||||
has_watch=0
|
has_watch=0
|
||||||
|
has_question=0
|
||||||
|
|
||||||
[[ "$unread" == "1" ]] && has_bell=1
|
[[ "$unread" == "1" ]] && has_bell=1
|
||||||
|
|
||||||
[[ "$watching" == "1" ]] && has_watch=1
|
[[ "$watching" == "1" ]] && has_watch=1
|
||||||
|
|
||||||
|
question_pane=$(tmux list-panes -t "$window_id" -F '#{@op_question_pending}' 2>/dev/null | grep -F -m1 -x '1' || true)
|
||||||
|
[[ -n "$question_pane" ]] && has_question=1
|
||||||
|
|
||||||
CACHE_FILE="/tmp/tmux-tracker-cache.json"
|
CACHE_FILE="/tmp/tmux-tracker-cache.json"
|
||||||
if [[ -f "$CACHE_FILE" ]]; then
|
if [[ -f "$CACHE_FILE" ]]; then
|
||||||
state=$(cat "$CACHE_FILE" 2>/dev/null || true)
|
state=$(cat "$CACHE_FILE" 2>/dev/null || true)
|
||||||
|
|
@ -30,7 +34,9 @@ if [[ -f "$CACHE_FILE" ]]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if (( has_bell )); then
|
if (( has_question )); then
|
||||||
|
printf '❓'
|
||||||
|
elif (( has_bell )); then
|
||||||
printf '🔔'
|
printf '🔔'
|
||||||
elif (( has_watch )); then
|
elif (( has_watch )); then
|
||||||
printf '⏳'
|
printf '⏳'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue