diff --git a/apps/desktop/src/components/common/QueryErrorActions.vue b/apps/desktop/src/components/common/QueryErrorActions.vue new file mode 100644 index 0000000000..c27f385b9a --- /dev/null +++ b/apps/desktop/src/components/common/QueryErrorActions.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index e79e2d0a32..8919c6092f 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -3,7 +3,7 @@ import { computed, ref, defineAsyncComponent, watch, nextTick, onMounted, onUnmo import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage"; import type { CSSProperties } from "vue"; import { useI18n } from "vue-i18n"; -import { Check, Columns3, EyeOff, Loader2, Search, Bot, GitBranch, BarChart3, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Timer, Wrench, Toolbox, ListChecks, Database, Download, Upload, X, Pin, Rows3, SquareDashed, Minus, Plus } from "@lucide/vue"; +import { Check, Columns3, EyeOff, Loader2, Search, GitBranch, BarChart3, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Timer, Wrench, Toolbox, ListChecks, Database, Download, Upload, X, Pin, Rows3, SquareDashed, Minus, Plus } from "@lucide/vue"; import { Splitpanes, Pane } from "splitpanes"; import "splitpanes/dist/splitpanes.css"; import { Button } from "@/components/ui/button"; @@ -14,6 +14,7 @@ import LightTooltip from "@/components/ui/LightTooltip.vue"; import QueryEditor from "@/components/editor/QueryEditor.vue"; import ColumnInfoPanel from "@/components/editor/ColumnInfoPanel.vue"; import QueryLoadingState from "@/components/common/QueryLoadingState.vue"; +import QueryErrorActions from "@/components/common/QueryErrorActions.vue"; import type { ColumnInfo } from "@/components/editor/ColumnInfoPanel.vue"; let dataGridComponentPromise: Promise | undefined; function loadDataGridComponent() { @@ -54,7 +55,6 @@ import { useConnectionStore } from "@/stores/connectionStore"; import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGridSearchMode } from "@/stores/settingsStore"; import { useToast } from "@/composables/useToast"; import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/sql/queryExecutionState"; -import { isQueryTimeoutErrorMessage } from "@/lib/sql/queryError"; import { databaseDisplayNameForTab, executionSummaryItems, nextExecutionSummaryView, resultGridCacheKey, resultRunItems, resultSqlForGrid, tabularResultItems } from "@/lib/tabs/tabPresentation"; import { defaultQueryResultArchiveFileName } from "@/lib/query/queryResultArchive"; import { saveQueryResultArchiveFile } from "@/lib/query/queryResultArchiveFile"; @@ -1113,14 +1113,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe @sort="(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string, mode?: DataGridSortMode) => emit('sort', column, columnIndex, direction, whereInput, mode)" > + > + +
diff --git a/apps/desktop/src/lib/__tests__/sql/queryError.spec.ts b/apps/desktop/src/lib/__tests__/sql/queryError.spec.ts index de7d24eb47..5015d9a5ac 100644 --- a/apps/desktop/src/lib/__tests__/sql/queryError.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/queryError.spec.ts @@ -23,6 +23,7 @@ describe("isQueryTimeoutErrorMessage", () => { it("detects agent RPC client-side timeout", () => { expect(isQueryTimeoutErrorMessage("Agent RPC call timed out (30s)")).toBe(true); expect(isQueryTimeoutErrorMessage("Agent RPC call timed out (120s)")).toBe(true); + expect(isQueryTimeoutErrorMessage("Agent RPC error (-1): Agent RPC call timed out (120s)")).toBe(true); }); it("does not classify unrelated errors as query timeouts", () => { diff --git a/apps/desktop/src/lib/sql/queryError.ts b/apps/desktop/src/lib/sql/queryError.ts index 6ceb0f2558..d8d8fcd1e2 100644 --- a/apps/desktop/src/lib/sql/queryError.ts +++ b/apps/desktop/src/lib/sql/queryError.ts @@ -5,7 +5,7 @@ export function isQueryTimeoutErrorMessage(message: string): boolean { // fallback when JDBC setQueryTimeout never fires (unsupported/unresponsive driver), so the // backend already treats it as a query timeout (is_agent_rpc_timeout_error in query.rs) — // surface the same action here to stay consistent with the backend. - if (lower.startsWith("agent rpc call timed out")) return true; + if (/\bagent rpc call timed out\b/.test(lower)) return true; if (/\b(?:canceling|cancelling|canceled|cancelled)\b[\s\S]{0,80}\bstatement\b[\s\S]{0,80}\btimeout\b/.test(lower)) return true; if (/\b(?:connection|connect|pool|checkout|metadata|loading|health check|cancel request|ssh|tunnel)\b/.test(lower)) return false; return (