+ {([
+ { label: t('ai_providers.table_col_identity'), colIndex: 1, className: undefined },
+ { label: t('common.base_url'), colIndex: 2, className: undefined },
+ ] as const).map(({ label, colIndex, className }) => (
+
+ {label}
+ {isResizable(colIndex) && (
+
onResizeStart(colIndex, e)}
+ onDoubleClick={() => resetColumn(colIndex)}
+ role="separator"
+ aria-orientation="vertical"
+ aria-label={t('common.resize_column')}
+ />
+ )}
+
+ ))}
{t('ai_providers.table_col_models')}
@@ -84,6 +103,7 @@ export function ProviderTable({
role="row"
tabIndex={0}
className={`${styles.row} ${row.enabled ? '' : styles.rowDisabled}`}
+ style={gridStyle}
onClick={() => onShowDetail(row)}
onKeyDown={(event) => handleRowKeyDown(event, row)}
aria-label={`${kindLabel} ${row.label}`}
diff --git a/apps/web/src/components/providers/ProviderTable/useResizableColumns.ts b/apps/web/src/components/providers/ProviderTable/useResizableColumns.ts
new file mode 100644
index 000000000..916a0f2d7
--- /dev/null
+++ b/apps/web/src/components/providers/ProviderTable/useResizableColumns.ts
@@ -0,0 +1,110 @@
+import { useCallback, useMemo, useRef, useState } from 'react';
+
+const STORAGE_KEY = 'providerTable.columnWidths';
+
+const COLUMN_DEFAULTS = [
+ '92px',
+ 'minmax(112px, 0.72fr)',
+ 'minmax(144px, 0.96fr)',
+ '80px',
+ '76px',
+ '360px',
+ '60px',
+ '108px',
+];
+
+const RESIZABLE_MIN_WIDTHS: Record
= {
+ 1: 112,
+ 2: 144,
+};
+
+function loadWidths(): Record {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (!raw) return {};
+ const parsed = JSON.parse(raw);
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
+ } catch {
+ /* ignore */
+ }
+ return {};
+}
+
+function saveWidths(widths: Record) {
+ try {
+ if (Object.keys(widths).length === 0) {
+ localStorage.removeItem(STORAGE_KEY);
+ } else {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(widths));
+ }
+ } catch {
+ /* ignore */
+ }
+}
+
+export function useResizableColumns() {
+ const [widths, setWidths] = useState(loadWidths);
+ const dragRef = useRef<{ col: number; startX: number; startW: number } | null>(null);
+
+ const gridTemplateColumns = useMemo(() => {
+ const parts = [...COLUMN_DEFAULTS];
+ for (const [col, w] of Object.entries(widths)) {
+ const i = Number(col);
+ if (i in RESIZABLE_MIN_WIDTHS) parts[i] = `${w}px`;
+ }
+ return parts.join(' ');
+ }, [widths]);
+
+ const onResizeStart = useCallback(
+ (colIndex: number, e: React.PointerEvent) => {
+ if (!(colIndex in RESIZABLE_MIN_WIDTHS)) return;
+ e.preventDefault();
+ e.stopPropagation();
+
+ const headerCell = e.currentTarget.parentElement as HTMLElement | null;
+ if (!headerCell) return;
+ const startW = headerCell.getBoundingClientRect().width;
+
+ dragRef.current = { col: colIndex, startX: e.clientX, startW };
+ document.body.style.cursor = 'col-resize';
+ document.body.style.userSelect = 'none';
+
+ const onMove = (ev: PointerEvent) => {
+ const state = dragRef.current;
+ if (!state) return;
+ const minW = RESIZABLE_MIN_WIDTHS[state.col] ?? 80;
+ const newW = Math.round(Math.max(minW, state.startW + ev.clientX - state.startX));
+ setWidths((prev) => ({ ...prev, [state.col]: newW }));
+ };
+
+ const onUp = () => {
+ dragRef.current = null;
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+ document.removeEventListener('pointermove', onMove);
+ document.removeEventListener('pointerup', onUp);
+ setWidths((prev) => {
+ saveWidths(prev);
+ return prev;
+ });
+ };
+
+ document.addEventListener('pointermove', onMove);
+ document.addEventListener('pointerup', onUp);
+ },
+ []
+ );
+
+ const resetColumn = useCallback((colIndex: number) => {
+ setWidths((prev) => {
+ const next = { ...prev };
+ delete next[colIndex];
+ saveWidths(next);
+ return next;
+ });
+ }, []);
+
+ const isResizable = useCallback((colIndex: number) => colIndex in RESIZABLE_MIN_WIDTHS, []);
+
+ return { gridTemplateColumns, onResizeStart, resetColumn, isResizable };
+}
diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json
index 723dd82e3..742284ec6 100644
--- a/apps/web/src/i18n/locales/en.json
+++ b/apps/web/src/i18n/locales/en.json
@@ -42,6 +42,7 @@
"prefix": "Prefix",
"proxy_url": "Proxy",
"priority": "Priority",
+ "resize_column": "Resize column",
"alias": "Alias",
"failure": "Failure",
"unknown_error": "Unknown error",
diff --git a/apps/web/src/i18n/locales/ru.json b/apps/web/src/i18n/locales/ru.json
index 72d69f5b3..4beb68d35 100644
--- a/apps/web/src/i18n/locales/ru.json
+++ b/apps/web/src/i18n/locales/ru.json
@@ -42,6 +42,7 @@
"prefix": "Префикс",
"proxy_url": "Прокси",
"priority": "Приоритет",
+ "resize_column": "Изменить ширину столбца",
"alias": "Псевдоним",
"failure": "Сбой",
"unknown_error": "Неизвестная ошибка",
diff --git a/apps/web/src/i18n/locales/zh-CN.json b/apps/web/src/i18n/locales/zh-CN.json
index 2790eeff4..9a5b46269 100644
--- a/apps/web/src/i18n/locales/zh-CN.json
+++ b/apps/web/src/i18n/locales/zh-CN.json
@@ -42,6 +42,7 @@
"prefix": "前缀",
"proxy_url": "代理",
"priority": "优先级",
+ "resize_column": "调整列宽",
"alias": "别名",
"failure": "失败",
"unknown_error": "未知错误",
diff --git a/apps/web/src/i18n/locales/zh-TW.json b/apps/web/src/i18n/locales/zh-TW.json
index e23306589..e442e2446 100644
--- a/apps/web/src/i18n/locales/zh-TW.json
+++ b/apps/web/src/i18n/locales/zh-TW.json
@@ -42,6 +42,7 @@
"prefix": "前綴",
"proxy_url": "代理",
"priority": "優先順序",
+ "resize_column": "調整欄寬",
"alias": "別名",
"failure": "失敗",
"unknown_error": "未知錯誤",