mirror of
https://github.com/cline/cline.git
synced 2026-09-24 23:20:16 +08:00
feat: queue and steer prompts in CLI & code app
This commit is contained in:
@@ -104,4 +104,5 @@ export type PromptInQueue = {
|
||||
id: string;
|
||||
prompt: string;
|
||||
steer: boolean;
|
||||
attachmentCount?: number;
|
||||
};
|
||||
|
||||
@@ -452,6 +452,10 @@ export function useChatSession() {
|
||||
? `${prompt}${prompt.length > 0 ? "\n\n" : ""}[attached ${attachmentCount} file${attachmentCount === 1 ? "" : "s"}]`
|
||||
: prompt;
|
||||
if (userLabel) {
|
||||
activeAssistantMessageIdRef.current = null;
|
||||
setActiveAssistantMessageId(null);
|
||||
clearLiveToolRefs();
|
||||
setStatus("running");
|
||||
addMessage({
|
||||
id: makeId("user"),
|
||||
sessionId: listeningSessionId,
|
||||
@@ -562,16 +566,10 @@ export function useChatSession() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
addMessage({
|
||||
id: makeId("tool"),
|
||||
sessionId: listeningSessionId,
|
||||
role: "tool",
|
||||
content: toolPayload,
|
||||
createdAt: Date.now(),
|
||||
meta: { toolName, hookEventName: "tool_call_end" },
|
||||
});
|
||||
// Ignore unmatched tool end events so stale completions from the
|
||||
// previous turn do not get rendered under a newer streaming turn.
|
||||
},
|
||||
[addMessage, appendMessageContent],
|
||||
[addMessage, appendMessageContent, clearLiveToolRefs],
|
||||
);
|
||||
|
||||
// ---- Transport / event subscriptions ----
|
||||
@@ -717,6 +715,7 @@ export function useChatSession() {
|
||||
action: "send",
|
||||
sessionId: activeSessionId,
|
||||
prompt: trimmed,
|
||||
delivery: shouldQueue ? "queue" : undefined,
|
||||
config: parsed,
|
||||
attachments: hasAttachments ? serializedAttachments : undefined,
|
||||
});
|
||||
@@ -823,6 +822,9 @@ export function useChatSession() {
|
||||
);
|
||||
}
|
||||
|
||||
const hasQueuedFollowUps =
|
||||
Array.isArray(payload.promptsInQueue) &&
|
||||
payload.promptsInQueue.length > 0;
|
||||
if (result?.finishReason === "error") {
|
||||
if (!resolvedAssistantText) {
|
||||
const toolError = Array.isArray(result?.toolCalls)
|
||||
@@ -839,6 +841,8 @@ export function useChatSession() {
|
||||
setStatus("failed");
|
||||
} else if (result?.finishReason === "aborted") {
|
||||
setStatus("cancelled");
|
||||
} else if (hasQueuedFollowUps) {
|
||||
setStatus("running");
|
||||
} else {
|
||||
setStatus("completed");
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
type JsonRecord,
|
||||
type LiveSession,
|
||||
type PromptInQueue,
|
||||
type QueuedChatTurn,
|
||||
type ToolApprovalRequestItem,
|
||||
} from "./types";
|
||||
|
||||
@@ -193,6 +192,53 @@ function handleBridgeStdoutLine(ctx: HostContext, parsed: JsonRecord) {
|
||||
);
|
||||
return;
|
||||
|
||||
case "pending_prompts": {
|
||||
const prompts = Array.isArray(parsed.prompts)
|
||||
? (
|
||||
parsed.prompts as Array<{
|
||||
id?: unknown;
|
||||
prompt?: unknown;
|
||||
delivery?: unknown;
|
||||
attachmentCount?: unknown;
|
||||
}>
|
||||
)
|
||||
.map((item) => ({
|
||||
id: typeof item.id === "string" ? item.id : "",
|
||||
prompt: typeof item.prompt === "string" ? item.prompt : "",
|
||||
steer: item.delivery === "steer",
|
||||
attachmentCount:
|
||||
typeof item.attachmentCount === "number"
|
||||
? item.attachmentCount
|
||||
: 0,
|
||||
}))
|
||||
.filter((item) => item.id && item.prompt)
|
||||
: [];
|
||||
if (sessionId) {
|
||||
const session = ctx.liveSessions.get(sessionId);
|
||||
const previous = session?.promptsInQueue ?? [];
|
||||
if (session) {
|
||||
session.promptsInQueue = prompts;
|
||||
}
|
||||
if (
|
||||
previous.length > prompts.length &&
|
||||
previous[0] &&
|
||||
previous[0].id !== prompts[0]?.id
|
||||
) {
|
||||
emitChunk(
|
||||
ctx,
|
||||
sessionId,
|
||||
"chat_queued_prompt_start",
|
||||
JSON.stringify({
|
||||
prompt: previous[0].prompt,
|
||||
attachmentCount: previous[0].attachmentCount ?? 0,
|
||||
}),
|
||||
);
|
||||
}
|
||||
sendPromptsInQueueSnapshot(ctx, sessionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case "error": {
|
||||
const message =
|
||||
typeof parsed.message === "string"
|
||||
@@ -338,16 +384,8 @@ export function broadcastApprovalSnapshots(ctx: HostContext) {
|
||||
}
|
||||
}
|
||||
|
||||
function makeQueuedTurnId(): string {
|
||||
return `queued_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function getPromptsInQueue(session: LiveSession): PromptInQueue[] {
|
||||
return session.pendingTurns.map((turn) => ({
|
||||
id: turn.id,
|
||||
prompt: turn.prompt,
|
||||
steer: turn.steer,
|
||||
}));
|
||||
return session.promptsInQueue;
|
||||
}
|
||||
|
||||
function sendPromptsInQueueSnapshot(ctx: HostContext, sessionId: string) {
|
||||
@@ -408,10 +446,18 @@ async function executeChatTurn(
|
||||
ctx: HostContext,
|
||||
sessionId: string,
|
||||
session: LiveSession,
|
||||
turn: QueuedChatTurn,
|
||||
): Promise<ChatTurnResult> {
|
||||
if (turn.config) session.config = turn.config;
|
||||
if (turn.prompt) session.prompt = turn.prompt;
|
||||
input: {
|
||||
prompt: string;
|
||||
config?: JsonRecord;
|
||||
attachments?: {
|
||||
userImages?: string[];
|
||||
userFiles?: Array<{ name: string; content: string }>;
|
||||
};
|
||||
delivery?: "queue" | "steer";
|
||||
},
|
||||
): Promise<{ result?: ChatTurnResult; queued?: boolean }> {
|
||||
if (input.config) session.config = input.config;
|
||||
if (input.prompt) session.prompt = input.prompt;
|
||||
|
||||
session.busy = true;
|
||||
session.status = "running";
|
||||
@@ -432,14 +478,19 @@ async function executeChatTurn(
|
||||
request: {
|
||||
config: session.config,
|
||||
messages: session.messages,
|
||||
prompt: turn.prompt,
|
||||
attachments: turn.attachments,
|
||||
prompt: input.prompt,
|
||||
attachments: input.attachments,
|
||||
delivery: input.delivery,
|
||||
},
|
||||
})) as { result?: ChatTurnResult };
|
||||
})) as { result?: ChatTurnResult; queued?: boolean };
|
||||
|
||||
if (resultEnvelope.queued) {
|
||||
return { queued: true };
|
||||
}
|
||||
const result = resultEnvelope.result;
|
||||
if (!result)
|
||||
if (!result) {
|
||||
throw new Error("chat runtime bridge send response missing result");
|
||||
}
|
||||
|
||||
session.messages = persistUsageInMessages(
|
||||
(Array.isArray(result.messages) ? result.messages : []) as unknown[],
|
||||
@@ -452,55 +503,12 @@ async function executeChatTurn(
|
||||
persistSessionMessages(sessionId, session.messages);
|
||||
sendApprovalSnapshot(ctx, sessionId);
|
||||
|
||||
return result;
|
||||
return { result, queued: false };
|
||||
} finally {
|
||||
session.busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function drainSessionQueue(
|
||||
ctx: HostContext,
|
||||
sessionId: string,
|
||||
): Promise<void> {
|
||||
const session = ctx.liveSessions.get(sessionId);
|
||||
if (!session || session.busy) return;
|
||||
|
||||
const nextTurn = session.pendingTurns.shift();
|
||||
if (!nextTurn) return;
|
||||
sendPromptsInQueueSnapshot(ctx, sessionId);
|
||||
emitChunk(
|
||||
ctx,
|
||||
sessionId,
|
||||
"chat_queued_prompt_start",
|
||||
JSON.stringify({
|
||||
prompt: nextTurn.prompt,
|
||||
attachmentCount:
|
||||
(nextTurn.attachments?.userImages?.length ?? 0) +
|
||||
(nextTurn.attachments?.userFiles?.length ?? 0),
|
||||
}),
|
||||
);
|
||||
|
||||
try {
|
||||
await executeChatTurn(ctx, sessionId, session, nextTurn);
|
||||
} catch (error) {
|
||||
session.status = "error";
|
||||
session.endedAt = nowMs();
|
||||
emitChunk(
|
||||
ctx,
|
||||
sessionId,
|
||||
"chat_core_log",
|
||||
JSON.stringify({
|
||||
level: "error",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (session.pendingTurns.length > 0) {
|
||||
void drainSessionQueue(ctx, sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Command handler
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -512,7 +520,7 @@ function createLiveSession(
|
||||
return {
|
||||
config,
|
||||
messages: [],
|
||||
pendingTurns: [],
|
||||
promptsInQueue: [],
|
||||
busy: false,
|
||||
startedAt: nowMs(),
|
||||
status: "idle",
|
||||
@@ -577,36 +585,23 @@ async function handleSend(
|
||||
}
|
||||
|
||||
if (request.config) session.config = request.config;
|
||||
|
||||
const turn: QueuedChatTurn = {
|
||||
id: makeQueuedTurnId(),
|
||||
const delivery =
|
||||
request.delivery === "queue" || request.delivery === "steer"
|
||||
? request.delivery
|
||||
: session.busy
|
||||
? "queue"
|
||||
: undefined;
|
||||
const { result, queued } = await executeChatTurn(ctx, sessionId, session, {
|
||||
prompt,
|
||||
steer: false,
|
||||
config: request.config,
|
||||
attachments: request.attachments,
|
||||
};
|
||||
|
||||
if (session.busy) {
|
||||
session.pendingTurns.push(turn);
|
||||
sendPromptsInQueueSnapshot(ctx, sessionId);
|
||||
return {
|
||||
sessionId,
|
||||
ok: true,
|
||||
queued: true,
|
||||
promptsInQueue: getPromptsInQueue(session),
|
||||
};
|
||||
}
|
||||
|
||||
const result = await executeChatTurn(ctx, sessionId, session, turn);
|
||||
if (session.pendingTurns.length > 0) {
|
||||
sendPromptsInQueueSnapshot(ctx, sessionId);
|
||||
void drainSessionQueue(ctx, sessionId);
|
||||
}
|
||||
delivery,
|
||||
});
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
result,
|
||||
queued: false,
|
||||
queued: queued === true,
|
||||
promptsInQueue: getPromptsInQueue(session),
|
||||
};
|
||||
}
|
||||
@@ -621,7 +616,7 @@ async function handleAbort(
|
||||
const session = ctx.liveSessions.get(sessionId);
|
||||
if (session) {
|
||||
session.busy = false;
|
||||
session.pendingTurns = [];
|
||||
session.promptsInQueue = [];
|
||||
session.status = "cancelled";
|
||||
session.endedAt = nowMs();
|
||||
}
|
||||
@@ -669,14 +664,16 @@ async function handleSteerPrompt(
|
||||
if (!session) {
|
||||
return { sessionId, promptsInQueue: [] };
|
||||
}
|
||||
const existingIndex = session.pendingTurns.findIndex(
|
||||
const prompt = session.promptsInQueue.find(
|
||||
(turn) => turn.id === promptId,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
const [turn] = session.pendingTurns.splice(existingIndex, 1);
|
||||
session.pendingTurns.unshift({ ...turn, steer: true });
|
||||
)?.prompt;
|
||||
if (prompt) {
|
||||
await executeChatTurn(ctx, sessionId, session, {
|
||||
prompt,
|
||||
config: session.config,
|
||||
delivery: "steer",
|
||||
});
|
||||
}
|
||||
sendPromptsInQueueSnapshot(ctx, sessionId);
|
||||
return {
|
||||
sessionId,
|
||||
promptsInQueue: getPromptsInQueue(session),
|
||||
|
||||
@@ -38,14 +38,7 @@ export type ChatSessionCommandRequest = {
|
||||
sessionId?: string;
|
||||
prompt?: string;
|
||||
promptId?: string;
|
||||
config?: JsonRecord;
|
||||
attachments?: ChatTurnAttachments;
|
||||
};
|
||||
|
||||
export type QueuedChatTurn = {
|
||||
id: string;
|
||||
prompt: string;
|
||||
steer: boolean;
|
||||
delivery?: "queue" | "steer";
|
||||
config?: JsonRecord;
|
||||
attachments?: ChatTurnAttachments;
|
||||
};
|
||||
@@ -54,12 +47,13 @@ export type PromptInQueue = {
|
||||
id: string;
|
||||
prompt: string;
|
||||
steer: boolean;
|
||||
attachmentCount?: number;
|
||||
};
|
||||
|
||||
export type LiveSession = {
|
||||
config: JsonRecord;
|
||||
messages: unknown[];
|
||||
pendingTurns: QueuedChatTurn[];
|
||||
promptsInQueue: PromptInQueue[];
|
||||
busy: boolean;
|
||||
startedAt: number;
|
||||
endedAt?: number;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { homedir } from "node:os";
|
||||
import {
|
||||
type RpcChatTurnResult,
|
||||
setHomeDir,
|
||||
setHomeDirIfUnset,
|
||||
} from "@clinebot/core";
|
||||
import { setHomeDir, setHomeDirIfUnset } from "@clinebot/core";
|
||||
import {
|
||||
type RpcRuntimeBridgeCommandOutputLine,
|
||||
runRpcRuntimeCommandBridge,
|
||||
@@ -82,7 +78,7 @@ async function main() {
|
||||
setRuntimeHomeDir(config);
|
||||
addRuntimeLoggerContext(config);
|
||||
},
|
||||
parseSendResult: (resultRaw) => resultRaw as RpcChatTurnResult,
|
||||
parseSendResult: (resultRaw) => resultRaw,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+242
-140
@@ -112,6 +112,7 @@ struct ChatRunTurnRequest {
|
||||
prompt: String,
|
||||
#[serde(default)]
|
||||
attachments: Option<ChatTurnAttachments>,
|
||||
delivery: Option<String>,
|
||||
}
|
||||
|
||||
fn default_agent_mode() -> String {
|
||||
@@ -225,6 +226,7 @@ struct ChatRuntimeBridgeLine {
|
||||
error: Option<String>,
|
||||
duration_ms: Option<u64>,
|
||||
message: Option<String>,
|
||||
prompts: Option<Vec<PendingPromptSnapshot>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -249,6 +251,8 @@ struct ChatSessionCommandRequest {
|
||||
action: String,
|
||||
session_id: Option<String>,
|
||||
prompt: Option<String>,
|
||||
prompt_id: Option<String>,
|
||||
delivery: Option<String>,
|
||||
config: Option<StartSessionRequest>,
|
||||
#[serde(default)]
|
||||
attachments: Option<ChatTurnAttachments>,
|
||||
@@ -261,6 +265,26 @@ struct ChatSessionCommandResponse {
|
||||
result: Option<ChatTurnResult>,
|
||||
ok: Option<bool>,
|
||||
queued: Option<bool>,
|
||||
#[serde(default)]
|
||||
prompts_in_queue: Vec<PromptInQueue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PendingPromptSnapshot {
|
||||
id: String,
|
||||
prompt: String,
|
||||
delivery: String,
|
||||
attachment_count: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PromptInQueue {
|
||||
id: String,
|
||||
prompt: String,
|
||||
steer: bool,
|
||||
attachment_count: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -350,7 +374,7 @@ impl ChatWsBridgeState {
|
||||
struct ChatRuntimeSession {
|
||||
config: StartSessionRequest,
|
||||
messages: Vec<Value>,
|
||||
pending_turns: Vec<ChatRunTurnRequest>,
|
||||
prompts_in_queue: Vec<PromptInQueue>,
|
||||
busy: bool,
|
||||
started_at: u64,
|
||||
ended_at: Option<u64>,
|
||||
@@ -2684,6 +2708,7 @@ fn ensure_chat_runtime_bridge_started(
|
||||
std::sync::mpsc::Sender<Result<Value, String>>,
|
||||
>::new()));
|
||||
let stdout_app = app.clone();
|
||||
let stdout_state = state.clone();
|
||||
let stdout_pending = pending.clone();
|
||||
thread::spawn(move || {
|
||||
let mut reader = BufReader::new(stdout);
|
||||
@@ -2765,6 +2790,40 @@ fn ensure_chat_runtime_bridge_started(
|
||||
payload.to_string(),
|
||||
);
|
||||
}
|
||||
"pending_prompts" => {
|
||||
let Some(session_id) = parsed.session_id.as_deref() else {
|
||||
continue;
|
||||
};
|
||||
let prompts = map_pending_prompts(parsed.prompts.unwrap_or_default());
|
||||
let previous = {
|
||||
let mut sessions = match stdout_state.sessions.lock() {
|
||||
Ok(value) => value,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let Some(session) = sessions.get_mut(session_id) else {
|
||||
continue;
|
||||
};
|
||||
let previous = session.prompts_in_queue.clone();
|
||||
session.prompts_in_queue = prompts.clone();
|
||||
previous
|
||||
};
|
||||
if previous.len() > prompts.len()
|
||||
&& !previous.is_empty()
|
||||
&& previous[0].id != prompts.first().map(|item| item.id.as_str()).unwrap_or("")
|
||||
{
|
||||
let payload = serde_json::json!({
|
||||
"prompt": previous[0].prompt,
|
||||
"attachmentCount": previous[0].attachment_count.unwrap_or(0),
|
||||
});
|
||||
emit_chunk(
|
||||
&stdout_app,
|
||||
session_id,
|
||||
"chat_queued_prompt_start",
|
||||
payload.to_string(),
|
||||
);
|
||||
}
|
||||
send_prompts_in_queue_snapshot(&stdout_app, &stdout_state, session_id);
|
||||
}
|
||||
"error" => {
|
||||
if let Some(session_id) = parsed.session_id.as_deref() {
|
||||
let payload = serde_json::json!({
|
||||
@@ -2990,7 +3049,7 @@ fn run_chat_turn_via_rpc_runtime(
|
||||
context: &AppContext,
|
||||
session_id: &str,
|
||||
request: &ChatRunTurnRequest,
|
||||
) -> Result<ChatTurnResult, String> {
|
||||
) -> Result<ChatSessionCommandResponse, String> {
|
||||
let response = run_chat_runtime_bridge_command(
|
||||
app,
|
||||
state,
|
||||
@@ -3001,12 +3060,20 @@ fn run_chat_turn_via_rpc_runtime(
|
||||
"request": request,
|
||||
}),
|
||||
)?;
|
||||
let result_value = response
|
||||
.get("result")
|
||||
.cloned()
|
||||
.ok_or_else(|| "chat runtime bridge send response missing result".to_string())?;
|
||||
serde_json::from_value::<ChatTurnResult>(result_value)
|
||||
.map_err(|e| format!("invalid chat runtime bridge result: {e}"))
|
||||
serde_json::from_value::<ChatSessionCommandResponse>(response)
|
||||
.map_err(|e| format!("invalid chat runtime bridge response: {e}"))
|
||||
}
|
||||
|
||||
fn map_pending_prompts(prompts: Vec<PendingPromptSnapshot>) -> Vec<PromptInQueue> {
|
||||
prompts
|
||||
.into_iter()
|
||||
.map(|item| PromptInQueue {
|
||||
id: item.id,
|
||||
prompt: item.prompt,
|
||||
steer: item.delivery == "steer",
|
||||
attachment_count: item.attachment_count,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn persist_chat_turn_result(
|
||||
@@ -3046,119 +3113,32 @@ fn mark_chat_turn_failed(state: &Arc<ChatSessionStore>, session_id: &str) -> Res
|
||||
.lock()
|
||||
.map_err(|_| "failed to lock chat session store")?;
|
||||
if let Some(session) = sessions.get_mut(session_id) {
|
||||
session.busy = false;
|
||||
session.status = "failed".to_string();
|
||||
session.ended_at = Some(now_ms());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dequeue_next_chat_turn(
|
||||
state: &Arc<ChatSessionStore>,
|
||||
session_id: &str,
|
||||
) -> Result<Option<(StartSessionRequest, ChatRunTurnRequest)>, String> {
|
||||
let mut sessions = state
|
||||
.sessions
|
||||
.lock()
|
||||
.map_err(|_| "failed to lock chat session store")?;
|
||||
let Some(session) = sessions.get_mut(session_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if session.pending_turns.is_empty() {
|
||||
session.busy = false;
|
||||
return Ok(None);
|
||||
}
|
||||
let mut next = session.pending_turns.remove(0);
|
||||
session.busy = true;
|
||||
session.status = "running".to_string();
|
||||
session.ended_at = None;
|
||||
if !next.prompt.trim().is_empty() {
|
||||
session.prompt = Some(next.prompt.clone());
|
||||
}
|
||||
next.config = session.config.clone();
|
||||
next.messages = session.messages.clone();
|
||||
Ok(Some((session.config.clone(), next)))
|
||||
}
|
||||
|
||||
fn queue_chat_turn(
|
||||
state: &Arc<ChatSessionStore>,
|
||||
session_id: &str,
|
||||
turn_request: ChatRunTurnRequest,
|
||||
) -> Result<(), String> {
|
||||
let mut sessions = state
|
||||
.sessions
|
||||
.lock()
|
||||
.map_err(|_| "failed to lock chat session store")?;
|
||||
let session = sessions
|
||||
.get_mut(session_id)
|
||||
.ok_or_else(|| "session not found. start a new session.".to_string())?;
|
||||
session.pending_turns.push(turn_request);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_next_queued_chat_turn(
|
||||
fn send_prompts_in_queue_snapshot(
|
||||
app: &AppHandle,
|
||||
state: &Arc<ChatSessionStore>,
|
||||
context: &AppContext,
|
||||
session_id: String,
|
||||
session_id: &str,
|
||||
) {
|
||||
let next = match dequeue_next_chat_turn(state, &session_id) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
eprintln!("[chat-queue] failed to dequeue next turn for {session_id}: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some((config, turn_request)) = next else {
|
||||
return;
|
||||
};
|
||||
|
||||
let app_for_turn = app.clone();
|
||||
let state_for_turn = state.clone();
|
||||
let context_for_turn = context.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let turn_result = tauri::async_runtime::spawn_blocking({
|
||||
let app_for_run = app_for_turn.clone();
|
||||
let state_for_run = state_for_turn.clone();
|
||||
let context_for_run = context_for_turn.clone();
|
||||
let session_id_for_run = session_id.clone();
|
||||
let request_for_run = turn_request.clone();
|
||||
move || {
|
||||
run_chat_turn_via_rpc_runtime(
|
||||
&app_for_run,
|
||||
&state_for_run,
|
||||
&context_for_run,
|
||||
&session_id_for_run,
|
||||
&request_for_run,
|
||||
)
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("chat turn task failed: {e}"));
|
||||
|
||||
match turn_result {
|
||||
Ok(Ok(result)) => {
|
||||
if let Err(error) =
|
||||
persist_chat_turn_result(&state_for_turn, &session_id, &config, &result)
|
||||
{
|
||||
eprintln!(
|
||||
"[chat-queue] failed to persist turn result for {session_id}: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Err(error) = mark_chat_turn_failed(&state_for_turn, &session_id) {
|
||||
eprintln!("[chat-queue] failed to mark turn failed for {session_id}: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spawn_next_queued_chat_turn(
|
||||
&app_for_turn,
|
||||
&state_for_turn,
|
||||
&context_for_turn,
|
||||
session_id,
|
||||
);
|
||||
});
|
||||
let items = state
|
||||
.sessions
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|sessions| sessions.get(session_id).cloned())
|
||||
.map(|session| session.prompts_in_queue)
|
||||
.unwrap_or_default();
|
||||
let payload = serde_json::json!({ "items": items });
|
||||
emit_chunk(
|
||||
app,
|
||||
session_id,
|
||||
"prompts_in_queue_state",
|
||||
payload.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
fn abort_chat_session_via_rpc_runtime(
|
||||
@@ -4265,7 +4245,7 @@ async fn handle_chat_session_command(
|
||||
ChatRuntimeSession {
|
||||
config,
|
||||
messages: Vec::new(),
|
||||
pending_turns: Vec::new(),
|
||||
prompts_in_queue: Vec::new(),
|
||||
busy: false,
|
||||
started_at: now_ms(),
|
||||
ended_at: None,
|
||||
@@ -4279,6 +4259,7 @@ async fn handle_chat_session_command(
|
||||
result: None,
|
||||
ok: None,
|
||||
queued: None,
|
||||
prompts_in_queue: Vec::new(),
|
||||
})
|
||||
}
|
||||
"send" => {
|
||||
@@ -4322,7 +4303,7 @@ async fn handle_chat_session_command(
|
||||
prompt: derive_prompt_from_messages(&messages),
|
||||
title: read_session_metadata_title(&session_id),
|
||||
messages,
|
||||
pending_turns: Vec::new(),
|
||||
prompts_in_queue: Vec::new(),
|
||||
busy: false,
|
||||
started_at: now_ms(),
|
||||
ended_at: None,
|
||||
@@ -4330,7 +4311,7 @@ async fn handle_chat_session_command(
|
||||
});
|
||||
}
|
||||
|
||||
let (config, messages) = {
|
||||
let (config, messages, delivery) = {
|
||||
let mut sessions = state
|
||||
.sessions
|
||||
.lock()
|
||||
@@ -4343,30 +4324,23 @@ async fn handle_chat_session_command(
|
||||
ensure_start_session_home_dir(&mut next_config);
|
||||
session.config = next_config;
|
||||
}
|
||||
if session.busy {
|
||||
let queued_request = ChatRunTurnRequest {
|
||||
config: session.config.clone(),
|
||||
messages: Vec::new(),
|
||||
prompt: prompt.clone(),
|
||||
attachments: attachments.clone(),
|
||||
};
|
||||
drop(sessions);
|
||||
queue_chat_turn(state, &session_id, queued_request)?;
|
||||
return Ok(ChatSessionCommandResponse {
|
||||
session_id: Some(session_id),
|
||||
result: None,
|
||||
ok: Some(true),
|
||||
queued: Some(true),
|
||||
});
|
||||
}
|
||||
let delivery = match request.delivery.as_deref() {
|
||||
Some("queue") => Some("queue".to_string()),
|
||||
Some("steer") => Some("steer".to_string()),
|
||||
_ if session.busy => Some("queue".to_string()),
|
||||
_ => None,
|
||||
};
|
||||
session.busy = true;
|
||||
session.status = "running".to_string();
|
||||
session.ended_at = None;
|
||||
if !prompt.is_empty() {
|
||||
// Keep sidebar/session discovery title in sync while turn is in-flight.
|
||||
session.prompt = Some(prompt.clone());
|
||||
}
|
||||
(session.config.clone(), session.messages.clone())
|
||||
(
|
||||
session.config.clone(),
|
||||
session.messages.clone(),
|
||||
delivery,
|
||||
)
|
||||
};
|
||||
|
||||
let session_id_for_turn = session_id.clone();
|
||||
@@ -4379,6 +4353,7 @@ async fn handle_chat_session_command(
|
||||
messages,
|
||||
prompt: prompt.clone(),
|
||||
attachments,
|
||||
delivery,
|
||||
};
|
||||
|
||||
let turn_result = tauri::async_runtime::spawn_blocking(move || {
|
||||
@@ -4393,20 +4368,62 @@ async fn handle_chat_session_command(
|
||||
.await
|
||||
.map_err(|e| format!("chat turn task failed: {e}"));
|
||||
|
||||
if let Ok(Ok(result)) = &turn_result {
|
||||
if let Ok(Ok(response)) = &turn_result {
|
||||
if response.queued == Some(true) {
|
||||
let prompts_in_queue = {
|
||||
let sessions = state
|
||||
.sessions
|
||||
.lock()
|
||||
.map_err(|_| "failed to lock chat session store")?;
|
||||
sessions
|
||||
.get(&session_id)
|
||||
.cloned()
|
||||
.map(|session| session.prompts_in_queue)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
return Ok(ChatSessionCommandResponse {
|
||||
session_id: Some(session_id),
|
||||
result: None,
|
||||
ok: Some(true),
|
||||
queued: Some(true),
|
||||
prompts_in_queue,
|
||||
});
|
||||
}
|
||||
let result = response
|
||||
.result
|
||||
.as_ref()
|
||||
.ok_or_else(|| "chat runtime bridge send response missing result".to_string())?;
|
||||
persist_chat_turn_result(state, &session_id, &config, result)?;
|
||||
let prompts_in_queue = {
|
||||
let mut sessions = state
|
||||
.sessions
|
||||
.lock()
|
||||
.map_err(|_| "failed to lock chat session store")?;
|
||||
let session = sessions
|
||||
.get_mut(&session_id)
|
||||
.ok_or_else(|| "session not found. start a new session.".to_string())?;
|
||||
session.busy = !session.prompts_in_queue.is_empty();
|
||||
session.prompts_in_queue.clone()
|
||||
};
|
||||
return Ok(ChatSessionCommandResponse {
|
||||
session_id: Some(session_id),
|
||||
result: Some(result.clone()),
|
||||
ok: None,
|
||||
queued: Some(false),
|
||||
prompts_in_queue,
|
||||
});
|
||||
} else {
|
||||
mark_chat_turn_failed(state, &session_id)?;
|
||||
}
|
||||
spawn_next_queued_chat_turn(app, state, context, session_id.clone());
|
||||
|
||||
let turn_result = turn_result?;
|
||||
let result = turn_result?;
|
||||
let response = turn_result?;
|
||||
Ok(ChatSessionCommandResponse {
|
||||
session_id: Some(session_id),
|
||||
result: Some(result),
|
||||
ok: None,
|
||||
queued: Some(false),
|
||||
result: response.result,
|
||||
ok: response.ok,
|
||||
queued: response.queued,
|
||||
prompts_in_queue: Vec::new(),
|
||||
})
|
||||
}
|
||||
"abort" => Ok(ChatSessionCommandResponse {
|
||||
@@ -4419,16 +4436,19 @@ async fn handle_chat_session_command(
|
||||
.map_err(|_| "failed to lock chat session store")?;
|
||||
if let Some(session) = sessions.get_mut(&session_id) {
|
||||
session.busy = false;
|
||||
session.pending_turns.clear();
|
||||
session.prompts_in_queue.clear();
|
||||
session.status = "cancelled".to_string();
|
||||
session.ended_at = Some(now_ms());
|
||||
}
|
||||
drop(sessions);
|
||||
send_prompts_in_queue_snapshot(app, state, &session_id);
|
||||
}
|
||||
request.session_id
|
||||
},
|
||||
result: None,
|
||||
ok: Some(true),
|
||||
queued: None,
|
||||
prompts_in_queue: Vec::new(),
|
||||
}),
|
||||
"reset" => {
|
||||
if let Some(session_id) = request.session_id.clone() {
|
||||
@@ -4444,12 +4464,94 @@ async fn handle_chat_session_command(
|
||||
Some(session_id.as_str()),
|
||||
);
|
||||
let _ = remove_chat_stream_subscription(app, state, context, &session_id);
|
||||
send_prompts_in_queue_snapshot(app, state, &session_id);
|
||||
}
|
||||
Ok(ChatSessionCommandResponse {
|
||||
session_id: request.session_id,
|
||||
result: None,
|
||||
ok: Some(true),
|
||||
queued: None,
|
||||
prompts_in_queue: Vec::new(),
|
||||
})
|
||||
}
|
||||
"pending_prompts" => {
|
||||
let Some(session_id) = request.session_id else {
|
||||
return Err("sessionId is required for pending_prompts action".to_string());
|
||||
};
|
||||
let prompts_in_queue = state
|
||||
.sessions
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|sessions| sessions.get(&session_id).cloned())
|
||||
.map(|session| session.prompts_in_queue)
|
||||
.unwrap_or_default();
|
||||
Ok(ChatSessionCommandResponse {
|
||||
session_id: Some(session_id),
|
||||
result: None,
|
||||
ok: Some(true),
|
||||
queued: None,
|
||||
prompts_in_queue,
|
||||
})
|
||||
}
|
||||
"steer_prompt" => {
|
||||
let Some(session_id) = request.session_id.clone() else {
|
||||
return Err("sessionId is required for steer_prompt action".to_string());
|
||||
};
|
||||
let Some(prompt_id) = request.prompt_id.clone() else {
|
||||
return Err("promptId is required for steer_prompt action".to_string());
|
||||
};
|
||||
let session = state
|
||||
.sessions
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|sessions| sessions.get(&session_id).cloned());
|
||||
if let Some(session) = session {
|
||||
let prompt = session
|
||||
.prompts_in_queue
|
||||
.iter()
|
||||
.find(|item| item.id == prompt_id)
|
||||
.map(|item| item.prompt.clone());
|
||||
if let Some(prompt) = prompt {
|
||||
ensure_chat_stream_subscription(app, state, context, &session_id)?;
|
||||
let _ = tauri::async_runtime::spawn_blocking({
|
||||
let app_for_turn = app.clone();
|
||||
let state_for_turn = state.clone();
|
||||
let context_for_turn = context.clone();
|
||||
let session_id_for_turn = session_id.clone();
|
||||
let turn_request = ChatRunTurnRequest {
|
||||
config: session.config.clone(),
|
||||
messages: session.messages.clone(),
|
||||
prompt,
|
||||
attachments: None,
|
||||
delivery: Some("steer".to_string()),
|
||||
};
|
||||
move || {
|
||||
run_chat_turn_via_rpc_runtime(
|
||||
&app_for_turn,
|
||||
&state_for_turn,
|
||||
&context_for_turn,
|
||||
&session_id_for_turn,
|
||||
&turn_request,
|
||||
)
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("chat turn task failed: {e}"))??;
|
||||
}
|
||||
}
|
||||
let prompts_in_queue = state
|
||||
.sessions
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|sessions| sessions.get(&session_id).cloned())
|
||||
.map(|session| session.prompts_in_queue)
|
||||
.unwrap_or_default();
|
||||
Ok(ChatSessionCommandResponse {
|
||||
session_id: Some(session_id),
|
||||
result: None,
|
||||
ok: Some(true),
|
||||
queued: Some(true),
|
||||
prompts_in_queue,
|
||||
})
|
||||
}
|
||||
_ => Err("unsupported action".to_string()),
|
||||
|
||||
Reference in New Issue
Block a user