fix(ai): improve assistant execution and MCP settings
This commit is contained in:
1 parent
1c18a80092
commit
5a666b1f48
20 files changed
+213
-47
No files matched your search
@@ -159,6 +159,10 @@ const MESSAGE_SCROLL_BUTTON_HIDE_THRESHOLD_PX = 48;
|
||||
let messageScrollViewport: HTMLElement | null = null;
|
||||
let messageTouchStartY: number | null = null;
|
||||
let lastMessageScrollTop = 0;
|
||||
let assistantDeltaFrame: number | null = null;
|
||||
let pendingAssistantDelta = "";
|
||||
let pendingAssistantReasoning = "";
|
||||
let pendingAssistantIndex = -1;
|
||||
|
||||
function startEditMessage(visibleIndex: number) {
|
||||
if (isGenerating.value) return;
|
||||
@@ -488,6 +492,12 @@ const proposalConfirmMessage = computed<ChatMessage | null>(() => {
|
||||
return null;
|
||||
});
|
||||
|
||||
let allowWriteSqlForNextRun = false;
|
||||
|
||||
function proposalContainsWriteSql(content: string) {
|
||||
return /\b(insert|update|delete|replace|merge|create|alter|drop|truncate|rename|grant|revoke)\b/i.test(content);
|
||||
}
|
||||
|
||||
function sendProposalReply(positive: boolean) {
|
||||
// Disable while a stream is in flight or no proposal is currently active.
|
||||
if (isGenerating.value) return;
|
||||
@@ -497,6 +507,7 @@ function sendProposalReply(positive: boolean) {
|
||||
const replyZh = positive ? "请执行上面你刚提议的操作,不要再反问确认。" : "不用执行上面提到的操作,继续当前对话。";
|
||||
const replyEn = positive ? "Execute the action you just proposed above; do not ask for confirmation again." : "Do not execute the action mentioned above; continue the current conversation.";
|
||||
prompt.value = isZh ? replyZh : replyEn;
|
||||
allowWriteSqlForNextRun = positive && assistantMode.value === "agent" && proposalContainsWriteSql(target.content);
|
||||
// Use the existing send pipeline so the message is added to history, persisted, etc.
|
||||
send();
|
||||
}
|
||||
@@ -596,19 +607,41 @@ function changeDatabase(value: string) {
|
||||
queryStore.updateDatabase(tab.id, decodeSelectableDatabaseValue(connection.db_type, value));
|
||||
}
|
||||
|
||||
function appendAssistantDelta(assistantIdx: number, delta: string) {
|
||||
const msg = messages.value[assistantIdx];
|
||||
if (msg.isThinking) msg.isThinking = false;
|
||||
msg.content += delta;
|
||||
function flushAssistantDeltas() {
|
||||
assistantDeltaFrame = null;
|
||||
const msg = messages.value[pendingAssistantIndex];
|
||||
if (!msg) return;
|
||||
if (pendingAssistantReasoning) {
|
||||
msg.reasoning = (msg.reasoning || "") + pendingAssistantReasoning;
|
||||
msg.isThinking = true;
|
||||
}
|
||||
if (pendingAssistantDelta) {
|
||||
msg.isThinking = false;
|
||||
msg.content += pendingAssistantDelta;
|
||||
}
|
||||
pendingAssistantDelta = "";
|
||||
pendingAssistantReasoning = "";
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function appendAssistantReasoning(assistantIdx: number, delta: string) {
|
||||
function scheduleAssistantDeltaFlush(assistantIdx: number) {
|
||||
pendingAssistantIndex = assistantIdx;
|
||||
if (assistantDeltaFrame !== null) return;
|
||||
// Providers can emit many tiny chunks. Render once per animation frame so
|
||||
// Markdown parsing, highlighting, and layout do not run for every token.
|
||||
assistantDeltaFrame = requestAnimationFrame(flushAssistantDeltas);
|
||||
}
|
||||
|
||||
function appendAssistantDelta(assistantIdx: number, delta: string) {
|
||||
const msg = messages.value[assistantIdx];
|
||||
if (!msg.reasoning) msg.reasoning = "";
|
||||
msg.reasoning += delta;
|
||||
msg.isThinking = true;
|
||||
scrollToBottom();
|
||||
if (msg.isThinking) msg.isThinking = false;
|
||||
pendingAssistantDelta += delta;
|
||||
scheduleAssistantDeltaFlush(assistantIdx);
|
||||
}
|
||||
|
||||
function appendAssistantReasoning(assistantIdx: number, delta: string) {
|
||||
pendingAssistantReasoning += delta;
|
||||
scheduleAssistantDeltaFlush(assistantIdx);
|
||||
}
|
||||
|
||||
const reasoningExpanded = ref(false);
|
||||
@@ -1358,6 +1391,8 @@ async function send() {
|
||||
|
||||
const requestedAction = activeAction.value;
|
||||
const requestedMode = assistantMode.value;
|
||||
const allowWriteSql = requestedMode === "agent" && allowWriteSqlForNextRun;
|
||||
allowWriteSqlForNextRun = false;
|
||||
isGenerating.value = true;
|
||||
messages.value.push({ role: "assistant", content: "" });
|
||||
const assistantIdx = messages.value.length - 1;
|
||||
@@ -1379,6 +1414,7 @@ async function send() {
|
||||
mode: requestedMode,
|
||||
instruction: modelInstruction,
|
||||
context,
|
||||
allowWriteSql,
|
||||
},
|
||||
history,
|
||||
(event: AgentEvent) => {
|
||||
@@ -1420,6 +1456,8 @@ async function send() {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
messages.value[assistantIdx].content = `Error: ${message}`;
|
||||
} finally {
|
||||
if (assistantDeltaFrame !== null) cancelAnimationFrame(assistantDeltaFrame);
|
||||
flushAssistantDeltas();
|
||||
const msg = messages.value[assistantIdx];
|
||||
if (msg) msg.isThinking = false;
|
||||
isGenerating.value = false;
|
||||
@@ -1628,6 +1666,7 @@ function stopResize() {
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (assistantDeltaFrame !== null) cancelAnimationFrame(assistantDeltaFrame);
|
||||
clearTimeout(mentionTimer);
|
||||
cancelStream();
|
||||
detachMessageScrollListener();
|
||||
@@ -1838,7 +1877,8 @@ async function openExternalUrl(url: string) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="(seg, j) in messageRenderer.render(msg.content)" :key="j">
|
||||
<div v-if="isGenerating && msg === messages[messages.length - 1]" class="whitespace-pre-wrap break-words text-sm leading-relaxed">{{ msg.content }}</div>
|
||||
<template v-else v-for="(seg, j) in messageRenderer.render(msg.content)" :key="j">
|
||||
<div v-if="seg.type === 'text'" class="ai-markdown whitespace-normal" @click.capture="onMarkdownClick">
|
||||
<div v-html="seg.html" />
|
||||
</div>
|
||||
|
||||
@@ -1301,8 +1301,10 @@ const mcpStatusLoading = ref(false);
|
||||
const mcpStatusError = ref("");
|
||||
const mcpCopied = ref<"" | McpCopyKind>("");
|
||||
const mcpConfigTab = ref<McpConfigTab>("claude");
|
||||
const mcpReadonlyMode = ref(false);
|
||||
const mcpAllowDangerous = ref(false);
|
||||
const MCP_READONLY_STORAGE_KEY = "dbx-mcp-config-readonly";
|
||||
const MCP_ALLOW_DANGEROUS_STORAGE_KEY = "dbx-mcp-config-allow-dangerous";
|
||||
const mcpReadonlyMode = ref(localStorage.getItem(MCP_READONLY_STORAGE_KEY) === "true");
|
||||
const mcpAllowDangerous = ref(localStorage.getItem(MCP_ALLOW_DANGEROUS_STORAGE_KEY) === "true");
|
||||
const mcpInstalling = ref(false);
|
||||
const mcpInstallMessage = ref("");
|
||||
const mcpInstallError = ref(false);
|
||||
@@ -1355,9 +1357,14 @@ const mcpCommand = computed(() => {
|
||||
});
|
||||
|
||||
watch(mcpReadonlyMode, (value) => {
|
||||
localStorage.setItem(MCP_READONLY_STORAGE_KEY, String(value));
|
||||
if (value) mcpAllowDangerous.value = false;
|
||||
});
|
||||
|
||||
watch(mcpAllowDangerous, (value) => {
|
||||
localStorage.setItem(MCP_ALLOW_DANGEROUS_STORAGE_KEY, String(value));
|
||||
});
|
||||
|
||||
async function refreshMcpStatus() {
|
||||
if (mcpStatusLoading.value) return;
|
||||
mcpStatusLoading.value = true;
|
||||
@@ -4025,6 +4032,7 @@ onUnmounted(cleanupPreviewEditor);
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs text-muted-foreground">{{ t("settings.mcpConfigOptionsHint") }}</p>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="mcp-readonly-mode">{{ t("settings.mcpReadonlyMode") }}</Label>
|
||||
|
||||
Reference in new issue
Block a user