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