diff --git a/apps/desktop/src/components/common/QueryErrorActions.vue b/apps/desktop/src/components/common/QueryErrorActions.vue
index c27f385b9a..924621594c 100644
--- a/apps/desktop/src/components/common/QueryErrorActions.vue
+++ b/apps/desktop/src/components/common/QueryErrorActions.vue
@@ -2,10 +2,12 @@
import { Bot, Wrench } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
+import type { BackendError } from "@/lib/backend/errorUtils";
import { isQueryTimeoutErrorMessage } from "@/lib/sql/queryError";
const props = defineProps<{
errorMessage: string;
+ backendError?: BackendError;
connectionId?: string;
}>();
@@ -18,7 +20,7 @@ const { t } = useI18n();
-
- emit('fixWithAi', message)" />
+ emit('fixWithAi', message)"
+ />
emit('sort', column, columnIndex, direction, whereInput, mode)"
>
- emit('fixWithAi', message)" />
+ emit('fixWithAi', message)"
+ />
diff --git a/apps/desktop/src/lib/__tests__/sql/queryError.spec.ts b/apps/desktop/src/lib/__tests__/sql/queryError.spec.ts
index 5015d9a5ac..0ea7f0136b 100644
--- a/apps/desktop/src/lib/__tests__/sql/queryError.spec.ts
+++ b/apps/desktop/src/lib/__tests__/sql/queryError.spec.ts
@@ -26,6 +26,33 @@ describe("isQueryTimeoutErrorMessage", () => {
expect(isQueryTimeoutErrorMessage("Agent RPC error (-1): Agent RPC call timed out (120s)")).toBe(true);
});
+ it("detects structured query operation timeouts before localized text matching", () => {
+ const timeoutError = {
+ version: 1,
+ code: "DBX-JDBC-2002",
+ messageKey: "backendErrors.jdbc.operationTimedOut",
+ messageParams: { stage: "execute" },
+ source: "jdbcAgent",
+ operationOutcome: "unknown" as const,
+ } as const;
+
+ expect(isQueryTimeoutErrorMessage("数据库操作超时(阶段:execute)。", timeoutError)).toBe(true);
+ expect(isQueryTimeoutErrorMessage("Database operation timed out (stage: fetch).", { ...timeoutError, messageParams: { stage: "fetch" } })).toBe(true);
+ });
+
+ it("does not offer query timeout settings for structured infrastructure timeouts", () => {
+ const timeoutError = {
+ version: 1,
+ code: "DBX-JDBC-2001",
+ messageKey: "backendErrors.jdbc.operationTimedOut",
+ messageParams: { stage: "connect" },
+ source: "jdbcAgent",
+ operationOutcome: "not_started" as const,
+ } as const;
+
+ expect(isQueryTimeoutErrorMessage("Database operation timed out (stage: connect).", timeoutError)).toBe(false);
+ });
+
it("does not classify unrelated errors as query timeouts", () => {
expect(isQueryTimeoutErrorMessage('syntax error at or near "select"')).toBe(false);
expect(isQueryTimeoutErrorMessage("Connection timed out while loading databases")).toBe(false);
diff --git a/apps/desktop/src/lib/sql/queryError.ts b/apps/desktop/src/lib/sql/queryError.ts
index d8d8fcd1e2..01501e0489 100644
--- a/apps/desktop/src/lib/sql/queryError.ts
+++ b/apps/desktop/src/lib/sql/queryError.ts
@@ -1,4 +1,12 @@
-export function isQueryTimeoutErrorMessage(message: string): boolean {
+import type { BackendError } from "@/lib/backend/errorUtils";
+
+const QUERY_TIMEOUT_STAGES = new Set(["execute", "fetch"]);
+
+export function isQueryTimeoutErrorMessage(message: string, backendError?: BackendError): boolean {
+ if (backendError?.messageKey === "backendErrors.jdbc.operationTimedOut") {
+ const stage = String(backendError.messageParams.stage ?? "").toLowerCase();
+ return QUERY_TIMEOUT_STAGES.has(stage);
+ }
const lower = message.toLowerCase();
if (lower.includes("query timed out") || lower.includes("查询超时") || lower.includes("查詢逾時")) return true;
// Agent RPC client-side timeout (tokio::time::timeout in agent_driver.rs). This is the
diff --git a/packages/app-tests/queryTimeout.test.ts b/packages/app-tests/queryTimeout.test.ts
index 6be396ad92..3ffbc10ec3 100644
--- a/packages/app-tests/queryTimeout.test.ts
+++ b/packages/app-tests/queryTimeout.test.ts
@@ -1,7 +1,10 @@
import { strict as assert } from "node:assert";
+import { readFileSync } from "node:fs";
import { test } from "vitest";
import { DEFAULT_QUERY_TIMEOUT_SECS, frontendQueryTimeoutSecsForSql, queryTimeoutSecsForConnection } from "../../apps/desktop/src/lib/sql/queryTimeout.ts";
+const contentAreaSource = readFileSync("apps/desktop/src/components/layout/ContentArea.vue", "utf8");
+
test("queryTimeoutSecsForConnection falls back to the default timeout", () => {
assert.equal(DEFAULT_QUERY_TIMEOUT_SECS, 30);
assert.equal(queryTimeoutSecsForConnection(undefined), DEFAULT_QUERY_TIMEOUT_SECS);
@@ -17,3 +20,14 @@ test("frontend query timeout scales with SQL statement count", () => {
assert.equal(frontendQueryTimeoutSecsForSql("/* prep */\nINSERT INTO users VALUES (1);\n-- keep batching\nINSERT INTO users VALUES (2);", "mysql", 30), 120);
assert.equal(frontendQueryTimeoutSecsForSql("INSERT INTO users VALUES (1); INSERT INTO users VALUES (2);", "mysql", 0), 0);
});
+
+test("query timeout actions target the connection that produced the result", () => {
+ const resultErrorActions = [...contentAreaSource.matchAll(//g)].map((match) => match[0]).filter((tag) => tag.includes(':backend-error="activeTab.result.error"'));
+
+ assert.equal(resultErrorActions.length, 2);
+ for (const tag of resultErrorActions) {
+ assert.match(tag, /:connection-id="activeResultConnectionId"/);
+ assert.match(tag, /emit\('openConnectionSettings', activeResultConnectionId, 'advanced'\)/);
+ assert.doesNotMatch(tag, /activeTab\.connectionId/);
+ }
+});