This commit is contained in:
Timothy Jaeryang Baek
2026-08-13 21:02:17 -06:00
parent ec36972c2b
commit 083e351441
10 changed files with 384 additions and 74 deletions
+135 -29
View File
@@ -77,6 +77,7 @@
getAllTags,
getChatById,
getTagsById,
resolveChatMessageToolCall,
updateChatById,
updateChatFolderIdById
} from '$lib/apis/chats';
@@ -423,6 +424,131 @@
};
};
const parseToolArguments = (args) => {
if (!args) {
return {};
}
let value = args;
while (typeof value === 'string') {
try {
value = JSON.parse(value);
} catch {
break;
}
}
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {};
};
const getPendingAskUserFromMessage = (message) => {
if (message?.role !== 'assistant' || !Array.isArray(message.output)) {
return null;
}
const call = message.output.find(
(item) =>
item?.type === 'function_call' &&
item?.name === 'ask_user' &&
item?.status === 'pending' &&
(item?.call_id || item?.id)
);
if (call) {
return { message, call, args: parseToolArguments(call.arguments) };
}
return null;
};
const findPendingAskUser = (chatHistory) => {
if (!chatHistory?.messages) {
return null;
}
const messages = chatHistory.currentId
? createMessagesList(chatHistory, chatHistory.currentId)
: Object.values(chatHistory.messages);
for (const message of [...messages].reverse()) {
const pending = getPendingAskUserFromMessage(message);
if (pending) return pending;
}
return null;
};
const messageHasPendingAskUser = (message) => {
return !!getPendingAskUserFromMessage(message);
};
const answerPendingAskUser = async (messageId, callId, answers, timedOut = false) => {
if (!$chatId || !messageId || !callId) {
return;
}
await resolveChatMessageToolCall(localStorage.token, $chatId, messageId, callId, 'answer', {
answers,
timed_out: timedOut
}).catch(async (error) => {
toast.error(`${error}`);
await loadChat();
});
};
const rejectPendingAskUser = async (messageId, callId) => {
if (!$chatId || !messageId || !callId) {
return;
}
await resolveChatMessageToolCall(
localStorage.token,
$chatId,
messageId,
callId,
'reject'
).catch(async (error) => {
toast.error(`${error}`);
await loadChat();
});
};
$: pendingAskUser = findPendingAskUser(history);
$: savedAskUserPrompt = pendingAskUser
? {
show: true,
questions: Array.isArray(pendingAskUser.args?.questions)
? pendingAskUser.args.questions
: [],
allowOther: pendingAskUser.args?.allow_other !== false,
timeoutMs: null,
onConfirm: (value) => {
void answerPendingAskUser(
pendingAskUser.message.id,
pendingAskUser.call.call_id || pendingAskUser.call.id,
value?.answers ?? {},
false
);
},
onCancel: () => {
void rejectPendingAskUser(
pendingAskUser.message.id,
pendingAskUser.call.call_id || pendingAskUser.call.id
);
}
}
: null;
$: socketAskUserPrompt = {
show: showAskUserDialog,
questions: askUserQuestions,
allowOther: askUserAllowOther,
timeoutMs: askUserTimeoutMs,
onConfirm: (value) => {
showAskUserDialog = false;
eventCallback(value);
},
onCancel: () => {
showAskUserDialog = false;
eventCallback({ status: 'cancelled', answers: {} });
}
};
const mergeChatVariableSchemas = (modelIds = [], availableModels = []) => {
const byKey: Record<string, any> = {};
const conflicts: any[] = [];
@@ -2175,7 +2301,11 @@
} else {
taskIds = null;
// No active tasks and message incomplete → generation was interrupted
if (currentMessage?.role === 'assistant' && !currentMessage.done) {
if (
currentMessage?.role === 'assistant' &&
!currentMessage.done &&
!messageHasPendingAskUser(currentMessage)
) {
currentMessage.done = true;
}
}
@@ -2586,6 +2716,7 @@
}
history.messages[message.id] = message;
history = history;
if (done) {
message.done = true;
@@ -4192,20 +4323,7 @@
{onUpdate}
messageQueue={$chatRequestQueues[$chatId] ?? []}
{chatTasks}
askUser={{
show: showAskUserDialog,
questions: askUserQuestions,
allowOther: askUserAllowOther,
timeoutMs: askUserTimeoutMs,
onConfirm: (value) => {
showAskUserDialog = false;
eventCallback(value);
},
onCancel: () => {
showAskUserDialog = false;
eventCallback({ status: 'cancelled', answers: {} });
}
}}
askUser={savedAskUserPrompt ?? socketAskUserPrompt}
onQueueSendNow={sendQueuedMessageNow}
onQueueEdit={editQueuedMessage}
onQueueDelete={deleteQueuedMessage}
@@ -4296,20 +4414,7 @@
{onUpdate}
messageQueue={$chatRequestQueues[$chatId] ?? []}
{chatTasks}
askUser={{
show: showAskUserDialog,
questions: askUserQuestions,
allowOther: askUserAllowOther,
timeoutMs: askUserTimeoutMs,
onConfirm: (value) => {
showAskUserDialog = false;
eventCallback(value);
},
onCancel: () => {
showAskUserDialog = false;
eventCallback({ status: 'cancelled', answers: {} });
}
}}
askUser={savedAskUserPrompt ?? socketAskUserPrompt}
onQueueSendNow={sendQueuedMessageNow}
onQueueEdit={editQueuedMessage}
onQueueDelete={deleteQueuedMessage}
@@ -4354,6 +4459,7 @@
{onUpload}
{onUpdate}
messageQueue={$chatRequestQueues[$chatId] ?? []}
askUser={savedAskUserPrompt ?? socketAskUserPrompt}
onQueueSendNow={sendQueuedMessageNow}
onQueueEdit={editQueuedMessage}
onQueueDelete={deleteQueuedMessage}
+12 -12
View File
@@ -153,11 +153,20 @@
export let history;
export let taskIds = null;
export let askUser: AskUserPrompt = {
show: false,
questions: [],
allowOther: true,
timeoutMs: null,
onConfirm: (_value: any) => {},
onCancel: () => {}
};
$: isActive =
(taskIds && taskIds.length > 0) ||
(history.currentId && history.messages[history.currentId]?.done != true) ||
generating;
!askUser?.show &&
((taskIds && taskIds.length > 0) ||
(history.currentId && history.messages[history.currentId]?.done != true) ||
generating);
$: canCompact = !!history?.currentId;
export let prompt = '';
@@ -182,15 +191,6 @@
export let onQueueEdit: (id: string) => void = () => {};
export let onQueueDelete: (id: string) => void = () => {};
export let onUpdate: (data?: { file?: any }) => void = () => {};
export let askUser: AskUserPrompt = {
show: false,
questions: [],
allowOther: true,
timeoutMs: null,
onConfirm: (_value: any) => {},
onCancel: () => {}
};
export let chatTasks = [];
let inputContent = null;
@@ -168,7 +168,7 @@
{/if}
</div>
{#if resolvable && pendingToolTokens.length === 1}
{#if resolvable && pendingToolTokens.length === 1 && pendingToolTokens[0]?.attributes?.name !== 'ask_user'}
{@const pendingCallId = pendingToolTokens[0]?.attributes?.id ?? ''}
<span class="flex gap-1 shrink-0">
<button
@@ -210,24 +210,26 @@
<span class="text-xs text-gray-500 dark:text-gray-400 flex-1 min-w-0 line-clamp-1">
{token?.attributes?.name ?? $i18n.t('tool')}
</span>
<span class="flex gap-1 shrink-0">
<button
type="button"
class="text-[0.6875rem] px-2.5 py-0.5 rounded-md text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-white/8 hover:bg-gray-200 dark:hover:bg-white/12 transition-colors duration-100 disabled:opacity-50"
disabled={!pendingCallId || resolvingCallId === pendingCallId}
on:click={() => onResolve(pendingCallId, true)}
>
{$i18n.t('Allow')}
</button>
<button
type="button"
class="text-[0.6875rem] px-2 py-0.5 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors duration-100 disabled:opacity-50"
disabled={!pendingCallId || resolvingCallId === pendingCallId}
on:click={() => onResolve(pendingCallId, false)}
>
{$i18n.t('Deny')}
</button>
</span>
{#if token?.attributes?.name !== 'ask_user'}
<span class="flex gap-1 shrink-0">
<button
type="button"
class="text-[0.6875rem] px-2.5 py-0.5 rounded-md text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-white/8 hover:bg-gray-200 dark:hover:bg-white/12 transition-colors duration-100 disabled:opacity-50"
disabled={!pendingCallId || resolvingCallId === pendingCallId}
on:click={() => onResolve(pendingCallId, true)}
>
{$i18n.t('Allow')}
</button>
<button
type="button"
class="text-[0.6875rem] px-2 py-0.5 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors duration-100 disabled:opacity-50"
disabled={!pendingCallId || resolvingCallId === pendingCallId}
on:click={() => onResolve(pendingCallId, false)}
>
{$i18n.t('Deny')}
</button>
</span>
{/if}
</div>
{/each}
</div>
@@ -145,7 +145,8 @@ function buildToolCallToken(item: OutputItem, toolOutputByCallId: Record<string,
}
return {
summary: status === 'pending' ? 'Tool Approval Needed' : isDone ? 'Tool Executed' : 'Executing...',
summary:
status === 'pending' ? 'Tool Approval Needed' : isDone ? 'Tool Executed' : 'Executing...',
text: getToolResultText(resultItem),
attributes: {
type: 'tool_calls',
@@ -299,6 +300,14 @@ export function buildOutputDisplayItems(output: OutputItem[] = []): OutputDispla
return;
}
if (
item?.type === 'function_call' &&
item.name === 'ask_user' &&
(item.status === 'pending' || item.status === 'in_progress')
) {
return;
}
if (item?.type && GROUPABLE_OUTPUT_TYPES.has(item.type)) {
const token = buildDetailToken(item, index === output.length - 1, toolOutputByCallId);
if (token) {
+9 -1
View File
@@ -66,7 +66,14 @@
export let onQueueSendNow: (id: string) => void = () => {};
export let onQueueEdit: (id: string) => void = () => {};
export let onQueueDelete: (id: string) => void = () => {};
export let askUser = {
show: false,
questions: [],
allowOther: true,
timeoutMs: null,
onConfirm: (_value: any) => {},
onCancel: () => {}
};
export let dragged = false;
@@ -251,6 +258,7 @@
{onQueueSendNow}
{onQueueEdit}
{onQueueDelete}
{askUser}
{onWebSearchToggle}
on:chatVariables
on:submit={(e) => {
@@ -93,12 +93,16 @@
$: result = resultContent || decode(attributes?.result ?? '');
$: files = parseJSONString(decode(attributes?.files ?? ''));
$: embeds = parseJSONString(decode(attributes?.embeds ?? ''));
$: isAskUser = attributes?.name === 'ask_user';
$: needsInput = isAskUser && attributes?.status === 'pending';
$: needsApproval = !isAskUser && attributes?.status === 'pending' && resolvable;
$: args =
open || (Array.isArray(embeds) && embeds.length > 0) ? decode(attributes?.arguments ?? '') : '';
open || needsApproval || needsInput || (Array.isArray(embeds) && embeds.length > 0)
? decode(attributes?.arguments ?? '')
: '';
$: isDone = attributes?.done === 'true';
$: needsApproval = attributes?.status === 'pending' && resolvable;
$: isRejected = attributes?.status === 'rejected';
$: isExecuting = !needsApproval && attributes?.done && attributes?.done !== 'true';
$: isExecuting = !needsApproval && !needsInput && attributes?.done && attributes?.done !== 'true';
$: parsedArgs = parseArguments(args);
$: parsedResult = parseJSONString(result);
@@ -178,6 +182,8 @@
<span class="hidden @md:inline font-normal">
{#if isDone}
{$i18n.t('View Result from {{NAME}}', { NAME: attributes.name })}
{:else if needsInput}
{$i18n.t('Input needed')}
{:else if needsApproval}
{$i18n.t('Allow {{NAME}}?', { NAME: attributes.name })}
{:else if isRejected}
@@ -188,7 +194,7 @@
</span>
</div>
{#if needsApproval}
{#if needsApproval && !isAskUser}
<span class="flex gap-1 shrink-0">
<button
type="button"
@@ -225,8 +231,8 @@
<div
class="border border-gray-50 dark:border-gray-850/30 rounded-2xl my-1.5 p-2.5 space-y-2"
>
<!-- Input -->
{#if args}
<!-- Input -->
<div>
<div
class="text-[0.625rem] uppercase tracking-wider font-normal text-gray-400 dark:text-gray-500 mb-1.5 px-1"