修复进行中会话有时无法被取消
This commit is contained in:
@@ -1454,11 +1454,13 @@ async fn run_conversation_loop<'a>(
|
|||||||
let cx = session.connection();
|
let cx = session.connection();
|
||||||
let sid = session.session_id().clone();
|
let sid = session.session_id().clone();
|
||||||
let prompt_request = PromptRequest::new(sid.clone(), prompt_blocks);
|
let prompt_request = PromptRequest::new(sid.clone(), prompt_blocks);
|
||||||
let prompt_response = cx
|
// Use Box::pin (heap) instead of tokio::pin! (stack) so the
|
||||||
.clone()
|
// future can be moved into a background task on cancel.
|
||||||
.send_request_to(Agent, prompt_request)
|
let mut prompt_response = Box::pin(
|
||||||
.block_task();
|
cx.clone()
|
||||||
tokio::pin!(prompt_response);
|
.send_request_to(Agent, prompt_request)
|
||||||
|
.block_task(),
|
||||||
|
);
|
||||||
let mut tracked_terminal_tool_calls: HashMap<String, TrackedTerminalToolCall> =
|
let mut tracked_terminal_tool_calls: HashMap<String, TrackedTerminalToolCall> =
|
||||||
HashMap::new();
|
HashMap::new();
|
||||||
let mut terminal_poll_interval = tokio::time::interval(
|
let mut terminal_poll_interval = tokio::time::interval(
|
||||||
@@ -1659,6 +1661,25 @@ async fn run_conversation_loop<'a>(
|
|||||||
RequestPermissionOutcome::Cancelled,
|
RequestPermissionOutcome::Cancelled,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
// Immediately emit TurnComplete so the frontend
|
||||||
|
// transitions out of "prompting" and the user can
|
||||||
|
// send new messages. Don't wait for the agent —
|
||||||
|
// it may be slow to respond or not respond at all.
|
||||||
|
let _ = handle.emit(
|
||||||
|
"acp://event",
|
||||||
|
AcpEvent::TurnComplete {
|
||||||
|
connection_id: conn_id.into(),
|
||||||
|
session_id: sid.0.to_string(),
|
||||||
|
stop_reason: "cancelled".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Drain the prompt response in the background so
|
||||||
|
// the SACP library doesn't log "receiver dropped"
|
||||||
|
// errors when the agent eventually responds.
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = prompt_response.await;
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
Some(ConnectionCommand::Disconnect) | None => {
|
Some(ConnectionCommand::Disconnect) | None => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
|
|||||||
@@ -209,6 +209,10 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
const statusUpdatedRef = useRef(false)
|
const statusUpdatedRef = useRef(false)
|
||||||
const selectedAgentRef = useRef(selectedAgent)
|
const selectedAgentRef = useRef(selectedAgent)
|
||||||
const createConversationPendingRef = useRef(false)
|
const createConversationPendingRef = useRef(false)
|
||||||
|
// When the turn finishes (cancel / complete) before createConversation
|
||||||
|
// resolves, we can't update the DB status yet. This ref records the
|
||||||
|
// desired status so the createConversation callback can apply it.
|
||||||
|
const deferredStatusRef = useRef<string | null>(null)
|
||||||
const externalIdSavedRef = useRef(false)
|
const externalIdSavedRef = useRef(false)
|
||||||
const sessionIdRef = useRef<string | null>(null)
|
const sessionIdRef = useRef<string | null>(null)
|
||||||
const syncCancelRef = useRef<(() => void) | null>(null)
|
const syncCancelRef = useRef<(() => void) | null>(null)
|
||||||
@@ -333,8 +337,21 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
syncCancelRef.current?.()
|
syncCancelRef.current?.()
|
||||||
syncCancelRef.current = null
|
syncCancelRef.current = null
|
||||||
|
|
||||||
|
const targetStatus =
|
||||||
|
connStatus === "disconnected" || connStatus === "error"
|
||||||
|
? null
|
||||||
|
: "pending_review"
|
||||||
|
|
||||||
const persistedId = dbConvIdRef.current
|
const persistedId = dbConvIdRef.current
|
||||||
if (!persistedId) return
|
if (!persistedId) {
|
||||||
|
// Conversation hasn't been persisted yet (createConversation still
|
||||||
|
// in flight). Record the desired status so the create callback
|
||||||
|
// can apply it once the DB row exists.
|
||||||
|
if (targetStatus) {
|
||||||
|
deferredStatusRef.current = targetStatus
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Async patch metadata (usage, duration_ms, model, session_stats)
|
// Async patch metadata (usage, duration_ms, model, session_stats)
|
||||||
if (persistedId > 0) {
|
if (persistedId > 0) {
|
||||||
@@ -344,8 +361,8 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connStatus !== "disconnected" && connStatus !== "error") {
|
if (targetStatus) {
|
||||||
updateConversationStatus(persistedId, "pending_review")
|
updateConversationStatus(persistedId, targetStatus)
|
||||||
.then(() => refreshConversations())
|
.then(() => refreshConversations())
|
||||||
.catch((e: unknown) =>
|
.catch((e: unknown) =>
|
||||||
console.error("[ConversationTabView] update status:", e)
|
console.error("[ConversationTabView] update status:", e)
|
||||||
@@ -573,7 +590,12 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
buildConversationDraftStorageKey(selectedAgent, newConversationId)
|
buildConversationDraftStorageKey(selectedAgent, newConversationId)
|
||||||
)
|
)
|
||||||
statusUpdatedRef.current = false
|
statusUpdatedRef.current = false
|
||||||
updateConversationStatus(newConversationId, "in_progress")
|
// If the turn already finished while we were creating the
|
||||||
|
// conversation, apply the deferred status directly instead
|
||||||
|
// of setting "in_progress" (which would never be updated).
|
||||||
|
const initialStatus = deferredStatusRef.current ?? "in_progress"
|
||||||
|
deferredStatusRef.current = null
|
||||||
|
updateConversationStatus(newConversationId, initialStatus)
|
||||||
.then(() => refreshConversations())
|
.then(() => refreshConversations())
|
||||||
.catch((e: unknown) =>
|
.catch((e: unknown) =>
|
||||||
console.error("[ConversationTabView] update status:", e)
|
console.error("[ConversationTabView] update status:", e)
|
||||||
|
|||||||
@@ -1893,22 +1893,11 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
const cancel = useCallback(
|
const cancel = useCallback(async (contextKey: string) => {
|
||||||
async (contextKey: string) => {
|
const conn = storeRef.current.connections.get(contextKey)
|
||||||
const conn = storeRef.current.connections.get(contextKey)
|
if (!conn) return
|
||||||
if (!conn) return
|
await acpCancel(conn.connectionId)
|
||||||
await acpCancel(conn.connectionId)
|
}, [])
|
||||||
// Optimistically transition status so the UI stops showing
|
|
||||||
// "responding" immediately. If the agent was slow to respond to
|
|
||||||
// the CancelNotification the frontend would otherwise stay stuck
|
|
||||||
// in the "prompting" state indefinitely.
|
|
||||||
const current = storeRef.current.connections.get(contextKey)
|
|
||||||
if (current?.status === "prompting") {
|
|
||||||
dispatch({ type: "STATUS_CHANGED", contextKey, status: "connected" })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
)
|
|
||||||
|
|
||||||
const respondPermission = useCallback(
|
const respondPermission = useCallback(
|
||||||
async (contextKey: string, requestId: string, optionId: string) => {
|
async (contextKey: string, requestId: string, optionId: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user