mirror of
https://github.com/theniceboy/.config.git
synced 2026-04-17 09:16:31 +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
|
||||
|
||||
- `set_work_summary` is mandatory protocol, not a suggestion.
|
||||
- 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`.
|
||||
- Call `set_work_summary` once you have enough context to label the work accurately, and always before edits, patches, or other committed actions.
|
||||
- Keep `set_work_summary` up to date during the turn.
|
||||
- You may call any tools before `set_work_summary` when you need more context.
|
||||
- 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: "..." })`.
|
||||
- `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.
|
||||
|
|
|
|||
|
|
@ -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_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 OP_STATE_DIR = `${STATE_ROOT}/op`;
|
||||
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 timestamp = new Date().toISOString();
|
||||
|
|
@ -70,17 +71,30 @@ export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
|||
}
|
||||
return `${tmuxContext.sessionName}:${tmuxContext.windowIndex}.${tmuxContext.paneIndex}`;
|
||||
};
|
||||
const persistPaneSessionMap = async (sessionID) => {
|
||||
const paneSessionStateFile = () => {
|
||||
const locator = paneLocator();
|
||||
if (!sessionID || !locator) return;
|
||||
const stateFile = `${OP_STATE_DIR}/loc_${sanitizeKey(locator)}`;
|
||||
if (!locator) return "";
|
||||
return `${OP_STATE_DIR}/loc_${sanitizeKey(locator)}`;
|
||||
};
|
||||
const persistPaneSessionMap = async (sessionID) => {
|
||||
const stateFile = paneSessionStateFile();
|
||||
if (!sessionID || !stateFile) return;
|
||||
try {
|
||||
mkdirSync(OP_STATE_DIR, { recursive: true });
|
||||
const tmpPath = `${stateFile}.tmp`;
|
||||
writeFileSync(tmpPath, `${sessionID}\n`, "utf8");
|
||||
renameSync(tmpPath, stateFile);
|
||||
} 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) => {
|
||||
|
|
@ -95,6 +109,60 @@ export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
|||
let taskActive = false;
|
||||
let currentSessionID = null;
|
||||
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 check = await $`test -x ${TRACKER_BIN}`.nothrow();
|
||||
|
|
@ -208,6 +276,10 @@ export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
|||
return {
|
||||
"tool.execute.before": async (input, output) => {
|
||||
if (input.tool === "question") {
|
||||
if (!rootSessionID && input?.sessionID) {
|
||||
rootSessionID = input.sessionID;
|
||||
}
|
||||
await applyQuestionPending(true);
|
||||
log("Question tool called:", {
|
||||
questions: output.args?.questions || "no questions",
|
||||
timestamp: new Date().toISOString()
|
||||
|
|
@ -216,6 +288,16 @@ export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
|||
},
|
||||
|
||||
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
|
||||
if (event?.type === "message.updated") {
|
||||
|
|
@ -254,7 +336,12 @@ export const TrackerNotifyPlugin = async ({ client, directory, $ }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const sessionChanged = sessionID !== rootSessionID;
|
||||
rootSessionID = sessionID;
|
||||
await persistPaneSessionMap(sessionID);
|
||||
if (sessionChanged) {
|
||||
await syncPendingQuestionState(sessionID);
|
||||
}
|
||||
if (event?.type !== "session.status") return;
|
||||
|
||||
const status = event?.properties?.status;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue