Skip to content

Commit b6704ac

Browse files
committed
fix: align permission events with OpenCode API and add always-allow option
1 parent b69ce13 commit b6704ac

7 files changed

Lines changed: 56 additions & 45 deletions

File tree

.github/FUNDING.yml

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,4 @@
11
# These are supported funding model platforms
22

33
github: [dimillian]
4-
patreon: # Replace with a single Patreon username
5-
open_collective: # Replace with a single Open Collective username
6-
ko_fi: # Replace with a single Ko-fi username
7-
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8-
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9-
liberapay: # Replace with a single Liberapay username
10-
issuehunt: # Replace with a single IssueHunt username
11-
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12-
polar: # Replace with a single Polar username
13-
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14-
thanks_dev: # Replace with a single thanks.dev username
15-
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
4+
buy_me_a_coffee: jacobjmc

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ cd src-tauri && cargo check
3030
cd src-tauri && cargo test
3131
```
3232

33+
## Credits & Support
34+
35+
OpenCodeMonitor is a fork of [CodexMonitor](https://github.com/Dimillian/CodexMonitor) by [Thomas Ricouard](https://github.com/Dimillian). The majority of this app's functionality comes from his excellent work.
36+
37+
**Support the original author:**
38+
- [Sponsor Thomas on GitHub](https://github.com/sponsors/Dimillian)
39+
- [Ice Cubes for Mastodon](https://apps.apple.com/app/ice-cubes-for-mastodon/id6444915884) — his open-source Mastodon client
40+
41+
**Support this fork:**
42+
- [Buy me a coffee](https://buymeacoffee.com/jacobjmc)
43+
3344
## License
3445

3546
MIT — see [LICENSE](LICENSE)

src-tauri/src/backend/event_translator.rs

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ pub(crate) fn translate_sse_event(
380380
"session.updated" => translate_session_created_or_updated(properties, state, true),
381381
"session.status" => translate_session_status(properties, state),
382382
"session.idle" => translate_session_idle(properties, state),
383-
"permission.updated" => translate_sse_permission(properties, state),
383+
"permission.asked" => translate_sse_permission(properties, state),
384384
"question.asked" => translate_question_asked(properties, state),
385385
"question.replied" => translate_question_completed(properties),
386386
"question.rejected" => translate_question_completed(properties),
@@ -1468,24 +1468,28 @@ fn translate_sse_permission(properties: &Value, state: &mut SessionTranslationSt
14681468
.get("sessionID")
14691469
.and_then(|v| v.as_str())
14701470
.unwrap_or(&state.session_id);
1471+
// OpenCode sends "permission" field, not "type"
14711472
let perm_type = properties
1472-
.get("type")
1473+
.get("permission")
14731474
.and_then(|v| v.as_str())
14741475
.unwrap_or("command");
1475-
let pattern = properties.get("pattern");
1476+
// OpenCode sends "patterns" array, not "pattern" string
1477+
let patterns = properties.get("patterns");
14761478

14771479
// Encode sessionId:permissionId into the event `id` so the frontend
14781480
// can round-trip it back to `respond_to_server_request`.
14791481
let composite_id = format!("{session_id}:{permission_id}");
14801482

1481-
let command_label = if let Some(pat) = pattern {
1482-
if let Some(arr) = pat.as_array() {
1483+
// Defensive handling: patterns can be array, string, or missing
1484+
let command_label = if let Some(pats) = patterns {
1485+
if let Some(arr) = pats.as_array() {
14831486
arr.iter()
14841487
.filter_map(|v| v.as_str())
14851488
.collect::<Vec<_>>()
14861489
.join(", ")
14871490
} else {
1488-
pat.as_str().unwrap_or(perm_type).to_string()
1491+
// Fallback if patterns is unexpectedly a string
1492+
pats.as_str().unwrap_or(perm_type).to_string()
14891493
}
14901494
} else {
14911495
perm_type.to_string()
@@ -1495,21 +1499,25 @@ fn translate_sse_permission(properties: &Value, state: &mut SessionTranslationSt
14951499
"id": composite_id,
14961500
"method": "codex/requestApproval",
14971501
"params": {
1498-
"threadId": session_id,
1499-
"type": perm_type,
1500-
"command": [command_label],
1501-
"rawInput": {}
1502+
"permission": perm_type,
1503+
"command": [command_label]
15021504
}
15031505
})]
15041506
}
15051507

15061508
/// Build the REST body for a permission decision.
15071509
///
1508-
/// `accept` = true → `{ response: "allow" }`
1509-
/// `accept` = false → `{ response: "deny" }`
1510-
pub(crate) fn build_permission_response(accept: bool) -> Value {
1511-
let response = if accept { "allow" } else { "deny" };
1512-
json!({ "response": response })
1510+
/// Maps frontend decisions to OpenCode reply format:
1511+
/// - `"accept"` → `{ reply: "once" }`
1512+
/// - `"always"` → `{ reply: "always" }`
1513+
/// - `"decline"` (or other) → `{ reply: "reject" }`
1514+
pub(crate) fn build_permission_response(decision: &str) -> Value {
1515+
let reply = match decision {
1516+
"accept" => "once",
1517+
"always" => "always",
1518+
_ => "reject",
1519+
};
1520+
json!({ "reply": reply })
15131521
}
15141522

15151523
// ---------------------------------------------------------------------------
@@ -2406,22 +2414,23 @@ mod tests {
24062414
}
24072415

24082416
#[test]
2409-
fn permission_updated_produces_approval_request() {
2417+
fn permission_asked_produces_approval_request() {
24102418
let mut state = make_state();
24112419
let event = json!({
2412-
"type": "permission.updated",
2420+
"type": "permission.asked",
24132421
"properties": {
24142422
"id": "perm_42",
2415-
"type": "bash",
2423+
"permission": "bash",
24162424
"sessionID": "ses_test123",
2417-
"pattern": "rm -rf /tmp/test"
2425+
"patterns": ["rm -rf /tmp/test"]
24182426
}
24192427
});
24202428
let events = translate_sse_event(&event, &mut state);
24212429
assert_eq!(events.len(), 1);
24222430
assert_eq!(events[0]["method"], "codex/requestApproval");
24232431
assert_eq!(events[0]["id"], "ses_test123:perm_42");
2424-
assert_eq!(events[0]["params"]["type"], "bash");
2432+
assert_eq!(events[0]["params"]["permission"], "bash");
2433+
assert_eq!(events[0]["params"]["command"][0], "rm -rf /tmp/test");
24252434
}
24262435

24272436
#[test]
@@ -2438,11 +2447,14 @@ mod tests {
24382447

24392448
#[test]
24402449
fn permission_response_shapes() {
2441-
let accept = build_permission_response(true);
2442-
assert_eq!(accept["response"], "allow");
2450+
let accept = build_permission_response("accept");
2451+
assert_eq!(accept["reply"], "once");
2452+
2453+
let always = build_permission_response("always");
2454+
assert_eq!(always["reply"], "always");
24432455

2444-
let deny = build_permission_response(false);
2445-
assert_eq!(deny["response"], "deny");
2456+
let deny = build_permission_response("decline");
2457+
assert_eq!(deny["reply"], "reject");
24462458
}
24472459

24482460
#[test]

src-tauri/src/shared/codex_core.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1811,12 +1811,11 @@ pub(crate) async fn respond_to_server_request_core(
18111811
session.rest_post_bool(&path, json!({})).await?;
18121812
} else {
18131813
// Permission response: POST /permission/:id/reply
1814-
let accept = result
1814+
let decision = result
18151815
.get("decision")
18161816
.and_then(|v| v.as_str())
1817-
.map(|d| d == "accept")
1818-
.unwrap_or(true);
1819-
let body = event_translator::build_permission_response(accept);
1817+
.unwrap_or("accept");
1818+
let body = event_translator::build_permission_response(decision);
18201819
let path = format!("/permission/{resource_id}/reply");
18211820
session.rest_post_bool(&path, body).await?;
18221821
}

src/features/app/components/RequestUserInputMessage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,12 @@ export function RequestUserInputMessage({
221221
)}
222222
</div>
223223
<div className="request-user-input-actions">
224-
<button className="primary" onClick={handleSubmit}>
225-
Submit
226-
</button>
227224
<button className="secondary" onClick={handleDismiss}>
228225
Dismiss
229226
</button>
227+
<button className="primary" onClick={handleSubmit}>
228+
Submit
229+
</button>
230230
<div className="request-user-input-shortcuts">
231231
<span><kbd>Enter</kbd> Submit</span>
232232
<span><kbd>Esc</kbd> Dismiss</span>

src/features/threads/hooks/useThreadApprovals.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function useThreadApprovals({ dispatch, onDebug }: UseThreadApprovalsOpti
7070
await respondToServerRequest(
7171
request.workspace_id,
7272
request.request_id,
73-
"accept",
73+
"always",
7474
);
7575
dispatch({
7676
type: "removeApproval",

src/services/tauri.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ export async function startReview(
359359
export async function respondToServerRequest(
360360
workspaceId: string,
361361
requestId: number | string,
362-
decision: "accept" | "decline",
362+
decision: "accept" | "decline" | "always",
363363
) {
364364
return invoke("respond_to_server_request", {
365365
workspaceId,

0 commit comments

Comments
 (0)