story-kit: merge 180_bug_web_ui_permissions_handling_unreliable

This commit is contained in:
Dave
2026-02-26 17:08:32 +00:00
parent ac087f1a58
commit d908a54fc4
2 changed files with 38 additions and 21 deletions

View File

@@ -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<{
const [permissionQueue, setPermissionQueue] = useState<
{
requestId: string;
toolName: string;
toolInput: Record<string, unknown>;
} | null>(null);
}[]
>([]);
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

View File

@@ -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"
);
}