story-kit: merge 91_bug_permissions_dialog_never_triggers_in_web_ui
This commit is contained in:
@@ -1,24 +1,12 @@
|
||||
use crate::slog;
|
||||
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::sync::watch;
|
||||
|
||||
use crate::llm::types::{FunctionCall, Message, Role, ToolCall};
|
||||
|
||||
/// A permission request emitted by Claude Code that must be resolved by the user.
|
||||
pub struct PermissionReqMsg {
|
||||
/// Unique identifier for this request (used to correlate the response).
|
||||
pub request_id: String,
|
||||
/// The tool that is requesting permission (e.g. "Bash", "Write").
|
||||
pub tool_name: String,
|
||||
/// The tool's input arguments (e.g. `{"command": "git push"}`).
|
||||
pub tool_input: serde_json::Value,
|
||||
/// One-shot channel to send the user's decision back to the PTY thread.
|
||||
pub response_tx: std::sync::mpsc::SyncSender<bool>,
|
||||
}
|
||||
|
||||
/// Result from a Claude Code session containing structured messages.
|
||||
pub struct ClaudeCodeResult {
|
||||
/// The conversation messages produced by Claude Code, including assistant
|
||||
@@ -37,6 +25,10 @@ pub struct ClaudeCodeResult {
|
||||
/// Supports session resumption: if a `session_id` is provided, passes
|
||||
/// `--resume <id>` so Claude Code loads the prior conversation transcript
|
||||
/// from disk and continues with full context.
|
||||
///
|
||||
/// Permissions are delegated to the MCP `prompt_permission` tool via
|
||||
/// `--permission-prompt-tool`, so Claude Code calls back into the server
|
||||
/// when a tool requires user approval. The frontend dialog handles the UX.
|
||||
pub struct ClaudeCodeProvider;
|
||||
|
||||
impl ClaudeCodeProvider {
|
||||
@@ -51,7 +43,6 @@ impl ClaudeCodeProvider {
|
||||
session_id: Option<&str>,
|
||||
cancel_rx: &mut watch::Receiver<bool>,
|
||||
mut on_token: F,
|
||||
permission_tx: Option<tokio::sync::mpsc::UnboundedSender<PermissionReqMsg>>,
|
||||
) -> Result<ClaudeCodeResult, String>
|
||||
where
|
||||
F: FnMut(&str) + Send,
|
||||
@@ -85,7 +76,6 @@ impl ClaudeCodeProvider {
|
||||
token_tx,
|
||||
msg_tx,
|
||||
sid_tx,
|
||||
permission_tx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -115,7 +105,10 @@ impl ClaudeCodeProvider {
|
||||
/// Sends streaming text tokens via `token_tx` for real-time display, and
|
||||
/// complete structured `Message` values via `msg_tx` for the final message
|
||||
/// history (assistant turns with tool_calls, and tool result turns).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
///
|
||||
/// Permission handling is delegated to the MCP `prompt_permission` tool
|
||||
/// via `--permission-prompt-tool`. Claude Code calls the MCP tool when it
|
||||
/// needs user approval, and the server bridges the request to the frontend.
|
||||
fn run_pty_session(
|
||||
user_message: &str,
|
||||
cwd: &str,
|
||||
@@ -124,7 +117,6 @@ fn run_pty_session(
|
||||
token_tx: tokio::sync::mpsc::UnboundedSender<String>,
|
||||
msg_tx: std::sync::mpsc::Sender<Message>,
|
||||
sid_tx: tokio::sync::oneshot::Sender<String>,
|
||||
permission_tx: Option<tokio::sync::mpsc::UnboundedSender<PermissionReqMsg>>,
|
||||
) -> Result<(), String> {
|
||||
let pty_system = native_pty_system();
|
||||
|
||||
@@ -147,6 +139,11 @@ fn run_pty_session(
|
||||
cmd.arg("--output-format");
|
||||
cmd.arg("stream-json");
|
||||
cmd.arg("--verbose");
|
||||
// Delegate permission decisions to the MCP prompt_permission tool.
|
||||
// Claude Code will call this tool via the story-kit MCP server when
|
||||
// a tool requires user approval, instead of using PTY stdin/stdout.
|
||||
cmd.arg("--permission-prompt-tool");
|
||||
cmd.arg("mcp__story-kit__prompt_permission");
|
||||
cmd.cwd(cwd);
|
||||
// Keep TERM reasonable but disable color
|
||||
cmd.env("NO_COLOR", "1");
|
||||
@@ -154,7 +151,7 @@ fn run_pty_session(
|
||||
cmd.env("CLAUDECODE", "");
|
||||
|
||||
slog!(
|
||||
"[pty-debug] Spawning: claude -p \"{}\" {} --output-format stream-json --verbose",
|
||||
"[pty-debug] Spawning: claude -p \"{}\" {} --output-format stream-json --verbose --permission-prompt-tool mcp__story-kit__prompt_permission",
|
||||
user_message,
|
||||
resume_session_id
|
||||
.map(|s| format!("--resume {s}"))
|
||||
@@ -177,11 +174,9 @@ fn run_pty_session(
|
||||
.try_clone_reader()
|
||||
.map_err(|e| format!("Failed to clone PTY reader: {e}"))?;
|
||||
|
||||
// Keep a writer handle so we can respond to permission_request events.
|
||||
let mut pty_writer = pair
|
||||
.master
|
||||
.take_writer()
|
||||
.map_err(|e| format!("Failed to take PTY writer: {e}"))?;
|
||||
// We no longer need the writer — permission responses flow through MCP,
|
||||
// not PTY stdin. Drop it so the PTY sees EOF on stdin when appropriate.
|
||||
drop(pair.master);
|
||||
|
||||
// Read NDJSON lines from stdout
|
||||
let (line_tx, line_rx) = std::sync::mpsc::channel::<Option<String>>();
|
||||
@@ -276,53 +271,6 @@ fn run_pty_session(
|
||||
"system" => {}
|
||||
// Rate limit info — suppress noisy notification
|
||||
"rate_limit_event" => {}
|
||||
// Claude Code is requesting user approval before executing a tool.
|
||||
// Forward the request to the async context via permission_tx and
|
||||
// block until the user responds (or a 5-minute timeout elapses).
|
||||
"permission_request" => {
|
||||
let request_id = json
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let tool_name = json
|
||||
.get("tool_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let tool_input = json
|
||||
.get("input")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
|
||||
|
||||
if let Some(ref ptx) = permission_tx {
|
||||
let (resp_tx, resp_rx) = std::sync::mpsc::sync_channel(1);
|
||||
let _ = ptx.send(PermissionReqMsg {
|
||||
request_id: request_id.clone(),
|
||||
tool_name,
|
||||
tool_input,
|
||||
response_tx: resp_tx,
|
||||
});
|
||||
// Block until the user responds or a 5-minute timeout elapses.
|
||||
let approved = resp_rx
|
||||
.recv_timeout(std::time::Duration::from_secs(300))
|
||||
.unwrap_or(false);
|
||||
let response = serde_json::json!({
|
||||
"type": "permission_response",
|
||||
"id": request_id,
|
||||
"approved": approved,
|
||||
});
|
||||
let _ = writeln!(pty_writer, "{}", response);
|
||||
} else {
|
||||
// No handler configured — deny by default.
|
||||
let response = serde_json::json!({
|
||||
"type": "permission_response",
|
||||
"id": request_id,
|
||||
"approved": false,
|
||||
});
|
||||
let _ = writeln!(pty_writer, "{}", response);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user