story-kit: merge 180_bug_web_ui_permissions_handling_unreliable
This commit is contained in:
@@ -169,11 +169,13 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
});
|
||||
const [claudeSessionId, setClaudeSessionId] = useState<string | null>(null);
|
||||
const [activityStatus, setActivityStatus] = useState<string | null>(null);
|
||||
const [permissionRequest, setPermissionRequest] = useState<{
|
||||
requestId: string;
|
||||
toolName: string;
|
||||
toolInput: Record<string, unknown>;
|
||||
} | null>(null);
|
||||
const [permissionQueue, setPermissionQueue] = useState<
|
||||
{
|
||||
requestId: string;
|
||||
toolName: string;
|
||||
toolInput: Record<string, unknown>;
|
||||
}[]
|
||||
>([]);
|
||||
const [isNarrowScreen, setIsNarrowScreen] = useState(
|
||||
window.innerWidth < NARROW_BREAKPOINT,
|
||||
);
|
||||
@@ -320,7 +322,10 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
setPipeline(state);
|
||||
},
|
||||
onPermissionRequest: (requestId, toolName, toolInput) => {
|
||||
setPermissionRequest({ requestId, toolName, toolInput });
|
||||
setPermissionQueue((prev) => [
|
||||
...prev,
|
||||
{ requestId, toolName, toolInput },
|
||||
]);
|
||||
},
|
||||
onActivity: (toolName) => {
|
||||
setActivityStatus(formatToolActivity(toolName));
|
||||
@@ -566,12 +571,10 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
};
|
||||
|
||||
const handlePermissionResponse = (approved: boolean) => {
|
||||
if (!permissionRequest) return;
|
||||
wsRef.current?.sendPermissionResponse(
|
||||
permissionRequest.requestId,
|
||||
approved,
|
||||
);
|
||||
setPermissionRequest(null);
|
||||
const current = permissionQueue[0];
|
||||
if (!current) return;
|
||||
wsRef.current?.sendPermissionResponse(current.requestId, approved);
|
||||
setPermissionQueue((prev) => prev.slice(1));
|
||||
};
|
||||
|
||||
const clearSession = async () => {
|
||||
@@ -975,7 +978,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{permissionRequest && (
|
||||
{permissionQueue.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
@@ -1002,6 +1005,17 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
>
|
||||
<h2 style={{ marginTop: 0, color: "#ececec" }}>
|
||||
Permission Request
|
||||
{permissionQueue.length > 1 && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.6em",
|
||||
color: "#888",
|
||||
marginLeft: "8px",
|
||||
}}
|
||||
>
|
||||
(+{permissionQueue.length - 1} queued)
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
@@ -1012,11 +1026,11 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
>
|
||||
The agent wants to use the{" "}
|
||||
<strong style={{ color: "#ececec" }}>
|
||||
{permissionRequest.toolName}
|
||||
{permissionQueue[0].toolName}
|
||||
</strong>{" "}
|
||||
tool. Do you approve?
|
||||
</p>
|
||||
{Object.keys(permissionRequest.toolInput).length > 0 && (
|
||||
{Object.keys(permissionQueue[0].toolInput).length > 0 && (
|
||||
<pre
|
||||
style={{
|
||||
background: "#1a1a1a",
|
||||
@@ -1032,7 +1046,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(permissionRequest.toolInput, null, 2)}
|
||||
{JSON.stringify(permissionQueue[0].toolInput, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
<div
|
||||
|
||||
@@ -1812,7 +1812,6 @@ async fn tool_prompt_permission(args: &Value, ctx: &AppContext) -> Result<String
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
let (response_tx, response_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
let tool_input_copy = tool_input.clone();
|
||||
ctx.perm_tx
|
||||
.send(crate::http::context::PermissionForward {
|
||||
request_id: request_id.clone(),
|
||||
@@ -1835,7 +1834,11 @@ async fn tool_prompt_permission(args: &Value, ctx: &AppContext) -> Result<String
|
||||
.map_err(|_| "Permission response channel closed unexpectedly".to_string())?;
|
||||
|
||||
if approved {
|
||||
Ok(json!({"updatedInput": tool_input_copy}).to_string())
|
||||
// Claude Code SDK validates the response as a union:
|
||||
// { behavior: "allow" } | { behavior: "deny", message: string }
|
||||
// Previously we returned {"updatedInput": ...} which didn't match
|
||||
// either variant, causing intermittent `invalid_union` errors.
|
||||
Ok(json!({"behavior": "allow"}).to_string())
|
||||
} else {
|
||||
slog_warn!("[permission] User denied permission for '{tool_name}'");
|
||||
Ok(json!({
|
||||
@@ -3371,7 +3374,7 @@ stage = "coder"
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tool_prompt_permission_approved_returns_updated_input_json() {
|
||||
async fn tool_prompt_permission_approved_returns_allow_behavior() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
|
||||
@@ -3393,8 +3396,8 @@ stage = "coder"
|
||||
|
||||
let parsed: Value = serde_json::from_str(&result).expect("result should be valid JSON");
|
||||
assert_eq!(
|
||||
parsed["updatedInput"]["command"], "echo hello",
|
||||
"approved must return updatedInput with original tool input"
|
||||
parsed["behavior"], "allow",
|
||||
"approved must return behavior:allow for Claude Code SDK compatibility"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user