mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
feat(agent-hooks): report the opencode session id (#481)
The opencode plugin now captures the session id from event properties and forwards it to the emitter on stdin, so a restarted pane can resume the same session with `opencode --session <id>`. Also maps session.status busy/idle for versions that no longer emit session.idle. A task-tool subagent runs in a child session whose events are structurally identical to the pane's own, so the bridge remembers child ids from session.created/session.updated and lets their events pass without touching the pane's session — otherwise the pane would report (and resume) the subagent's session, and a subagent going idle would call the pane done.
This commit is contained in:
@@ -922,27 +922,59 @@ fn opencode_plugin_js(target: &HookTarget) -> Option<String> {
|
||||
export const Tty7Presence = async ({{ $ }}) => {{
|
||||
if (!process.env["TTY7"]) return {{}}
|
||||
const cmd = {prefix}
|
||||
const emit = (event) => $`sh -c ${{cmd + event}}`.quiet().nothrow()
|
||||
|
||||
// Plugin load = the agent is running in this pane.
|
||||
await emit("session-start")
|
||||
let sessionId = ""
|
||||
let announced = ""
|
||||
// A subagent runs in a child session on the *same* event stream, and its
|
||||
// status events are indistinguishable from the pane's own — only
|
||||
// `session.created`/`session.updated` name a parent. Remember the children,
|
||||
// so a task tool cannot hand the pane the wrong session to resume, nor call
|
||||
// the pane done when only the subagent is.
|
||||
const children = new Set()
|
||||
const emit = (event) => {{
|
||||
// Every event carries the session id (`properties.sessionID`), so a pane
|
||||
// that restarts can resume the same session with `opencode --session`.
|
||||
const payload = sessionId ? new Response(JSON.stringify({{ session_id: sessionId }})) : undefined
|
||||
const proc = payload ? $`sh -c ${{cmd + event}} < ${{payload}}` : $`sh -c ${{cmd + event}}`
|
||||
return proc.quiet().nothrow()
|
||||
}}
|
||||
// The session id is only known once opencode creates the session (the
|
||||
// `session.created` event); the session-start report rides on the first
|
||||
// event that names one, so a restored pane can reattach to it.
|
||||
const capture = async (id) => {{
|
||||
if (id && !children.has(id)) sessionId = id
|
||||
if (sessionId && sessionId !== announced) {{
|
||||
announced = sessionId
|
||||
await emit("session-start")
|
||||
}}
|
||||
}}
|
||||
const ACTION = {{
|
||||
"session.status.busy": "prompt-submit",
|
||||
"session.status.idle": "stop",
|
||||
"session.idle": "stop",
|
||||
"permission.replied": "prompt-submit",
|
||||
}}
|
||||
|
||||
return {{
|
||||
dispose: async () => {{
|
||||
await emit("session-end")
|
||||
}},
|
||||
"tool.execute.before": async () => {{
|
||||
"tool.execute.before": async (input) => {{
|
||||
await capture(input?.sessionID)
|
||||
await emit("prompt-submit")
|
||||
}},
|
||||
"permission.ask": async () => {{
|
||||
"permission.ask": async (input) => {{
|
||||
await capture(input?.sessionID)
|
||||
await emit("permission-request")
|
||||
}},
|
||||
event: async ({{ event }}) => {{
|
||||
if (event.type === "session.idle") {{
|
||||
await emit("stop")
|
||||
}} else if (event.type === "permission.replied") {{
|
||||
await emit("prompt-submit")
|
||||
}}
|
||||
const properties = event.properties ?? {{}}
|
||||
const info = properties.info
|
||||
if (info?.id && info.parentID) children.add(info.id)
|
||||
if (properties.sessionID && children.has(properties.sessionID)) return
|
||||
await capture(properties.sessionID)
|
||||
const key = event.type === "session.status" ? `session.status.${{properties.status?.type}}` : event.type
|
||||
const action = ACTION[key]
|
||||
if (action) await emit(action)
|
||||
}},
|
||||
}}
|
||||
}}
|
||||
@@ -1386,6 +1418,34 @@ mod tests {
|
||||
assert!(opencode.contains("agent-hook opencode"));
|
||||
assert!(opencode.contains(hook_exe));
|
||||
assert!(opencode.contains(r#"process.env["TTY7"]"#));
|
||||
for (needle, message) in [
|
||||
(
|
||||
"properties.sessionID",
|
||||
"opencode captures the session id from event properties",
|
||||
),
|
||||
(
|
||||
r#"session_id: sessionId"#,
|
||||
"opencode forwards the session id to the emitter",
|
||||
),
|
||||
(
|
||||
"session.status",
|
||||
"opencode maps session.status busy/idle to prompt-submit/stop",
|
||||
),
|
||||
(
|
||||
"session.idle",
|
||||
"opencode still maps the session.idle event to stop",
|
||||
),
|
||||
(
|
||||
"info.parentID",
|
||||
"opencode tells a subagent's child session apart from the pane's own",
|
||||
),
|
||||
(
|
||||
"children.has(properties.sessionID)",
|
||||
"opencode lets a child session's events pass without touching the pane",
|
||||
),
|
||||
] {
|
||||
assert!(opencode.contains(needle), "{message}");
|
||||
}
|
||||
|
||||
for (agent, slug, package) in [
|
||||
(HookAgent::Pi, "pi", "@mariozechner/pi-coding-agent"),
|
||||
|
||||
Reference in New Issue
Block a user