Skip to content

Commit 7bf2b3d

Browse files
committed
Fix agent state hooks: versioned settings, CWD extraction, Codex Start, OpenCode error handling
- Version claude-settings.json with _v key so hooks get rewritten on updates - Register PermissionResponse hook to unstick sidebar after permission approval - Extract cwd from JSON payload in notify.sh instead of relying on pwd - Emit synthetic Start event in Codex wrapper (Codex only emits Stop) - Use worktree/directory from OpenCode plugin input instead of process.cwd() - Handle null sessionID in OpenCode error events to unstick working state
1 parent c86048e commit 7bf2b3d

2 files changed

Lines changed: 36 additions & 11 deletions

File tree

macos/Sources/Features/Worktrunk/AgentStatus/AgentHookInstaller.swift

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import Foundation
22

33
enum AgentHookInstaller {
4-
private static let notifyScriptMarker = "# Ghostree agent notification hook v5"
5-
private static let wrapperMarker = "# Ghostree agent wrapper v2"
4+
private static let notifyScriptMarker = "# Ghostree agent notification hook v6"
5+
private static let claudeSettingsMarker = "\"_v\":2"
6+
private static let wrapperMarker = "# Ghostree agent wrapper v3"
67

78
static func ensureInstalled() {
89
if ProcessInfo.processInfo.environment["GHOSTREE_DISABLE_AGENT_HOOKS"] == "1" {
@@ -35,7 +36,7 @@ enum AgentHookInstaller {
3536
ensureFile(
3637
url: AgentStatusPaths.claudeSettingsPath,
3738
mode: 0o644,
38-
marker: "bash '",
39+
marker: claudeSettingsMarker,
3940
content: buildClaudeSettings(notifyPath: AgentStatusPaths.notifyHookPath.path)
4041
)
4142
ensureFile(
@@ -129,7 +130,12 @@ enum AgentHookInstaller {
129130
[ "$EVENT_TYPE" = "SessionEnd" ] && EVENT_TYPE="SessionEnd"
130131
[ -z "$EVENT_TYPE" ] && exit 0
131132
132-
CWD="$(pwd -P 2>/dev/null || pwd)"
133+
JSON_CWD=$(echo "$INPUT" | grep -oE '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | grep -oE '"[^"]*"$' | tr -d '"')
134+
if [ -n "$JSON_CWD" ]; then
135+
CWD="$JSON_CWD"
136+
else
137+
CWD="$(pwd -P 2>/dev/null || pwd)"
138+
fi
133139
TS="$(perl -MTime::HiRes=time -MPOSIX=strftime -e '$t=time; $s=int($t); $ms=int(($t-$s)*1000); print strftime(\"%Y-%m-%dT%H:%M:%S\", gmtime($s)).sprintf(\".%03dZ\", $ms);')"
134140
ESC_CWD="${CWD//\\\\/\\\\\\\\}"
135141
ESC_CWD="${ESC_CWD//\\\"/\\\\\\\"}"
@@ -142,6 +148,7 @@ enum AgentHookInstaller {
142148
let escapedNotifyPath = notifyPath.replacingOccurrences(of: "'", with: "'\\''")
143149
let command = "bash '\(escapedNotifyPath)'"
144150
let settings: [String: Any] = [
151+
"_v": 2,
145152
"hooks": [
146153
"UserPromptSubmit": [
147154
["hooks": [["type": "command", "command": command]]],
@@ -152,6 +159,9 @@ enum AgentHookInstaller {
152159
"PermissionRequest": [
153160
["matcher": "*", "hooks": [["type": "command", "command": command]]],
154161
],
162+
"PermissionResponse": [
163+
["hooks": [["type": "command", "command": command]]],
164+
],
155165
"SessionEnd": [
156166
["hooks": [["type": "command", "command": command]]],
157167
],
@@ -226,6 +236,7 @@ enum AgentHookInstaller {
226236
private static func buildCodexWrapper() -> String {
227237
let binDir = AgentStatusPaths.binDir.path
228238
let notifyPath = AgentStatusPaths.notifyHookPath.path
239+
let eventsDir = AgentStatusPaths.eventsCacheDir.path
229240
return """
230241
#!/bin/bash
231242
\(wrapperMarker)
@@ -256,6 +267,12 @@ enum AgentHookInstaller {
256267
exit 127
257268
fi
258269
270+
# Emit synthetic Start event for Codex
271+
printf '{"timestamp":"%s","eventType":"Start","cwd":"%s"}\\n' \
272+
"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
273+
"$(pwd -P 2>/dev/null || pwd)" \
274+
>> "${GHOSTREE_AGENT_EVENTS_DIR:-\(eventsDir)}/agent-events.jsonl" 2>/dev/null
275+
259276
exec "$REAL_BIN" -c 'notify=["bash","\(notifyPath)"]' "$@"
260277
"""
261278
}
@@ -273,21 +290,22 @@ enum AgentHookInstaller {
273290
import fs from "node:fs";
274291
import path from "node:path";
275292
276-
export const GhostreeNotifyPlugin = async ({ client }) => {
277-
if (globalThis.__ghostreeOpencodeNotifyPluginV3) return {};
278-
globalThis.__ghostreeOpencodeNotifyPluginV3 = true;
279-
293+
export const GhostreeNotifyPlugin = async ({ client, directory, worktree }) => {
294+
if (globalThis.__ghostreeOpencodeNotifyPluginV4) return {};
295+
globalThis.__ghostreeOpencodeNotifyPluginV4 = true;
296+
280297
const eventsDir = process?.env?.GHOSTREE_AGENT_EVENTS_DIR;
281298
if (!eventsDir) return {};
282299
const logPath = path.join(eventsDir, "agent-events.jsonl");
283-
300+
const cwd = worktree || directory || process.cwd();
301+
284302
const append = (eventType) => {
285303
try {
286304
fs.mkdirSync(eventsDir, { recursive: true });
287305
const payload = {
288306
timestamp: new Date().toISOString(),
289307
eventType,
290-
cwd: process.cwd(),
308+
cwd,
291309
};
292310
fs.appendFileSync(logPath, JSON.stringify(payload) + "\\n");
293311
} catch {
@@ -334,6 +352,13 @@ enum AgentHookInstaller {
334352
};
335353
336354
const handleStop = async (sessionID) => {
355+
if (!sessionID && currentState === "busy" && !stopSent) {
356+
currentState = "idle";
357+
stopSent = true;
358+
append("Stop");
359+
rootSessionID = null;
360+
return;
361+
}
337362
const sid = normalizeSessionID(sessionID);
338363
if (rootSessionID && sid !== rootSessionID) return;
339364
if (currentState === "busy" && !stopSent) {

macos/Sources/Features/Worktrunk/AgentStatus/AgentStatusPaths.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ enum AgentStatusPaths {
4646
binDir.appendingPathComponent("codex")
4747
}
4848

49-
static var opencodePluginMarker: String { "// Ghostree opencode plugin v3" }
49+
static var opencodePluginMarker: String { "// Ghostree opencode plugin v4" }
5050

5151
/** @see https://opencode.ai/docs/plugins */
5252
static var opencodeGlobalPluginPath: URL {

0 commit comments

Comments
 (0)