fix(ui,runtime,search): stabilize markdown/table rendering, runtime notices, and title/search config
This commit bundles a coordinated fix set across frontend and backend runtime flows:\n\n1) Markdown table overflow UX\n- Wrap rendered markdown tables in a dedicated horizontal-scroll container.\n- Preserve wrapper attributes through sanitize so styles always apply.\n- Keep page-level horizontal scroll disabled while allowing in-container table scroll.\n- Improve table container visual consistency (border/radius/shadow/scrollbar behavior).\n\n2) Code block horizontal jitter\n- Adjust code/pre scrollbar gutter and box model handling to remove left-right jump.\n\n3) Execution mode auto-fallback sync + toast behavior\n- Add expiry-driven frontend sync timer for direct->sandbox fallback state refresh.\n- Ensure permission/execution UI state updates at expiry time.\n- Keep fallback warning toast persistent but avoid false-positive toasts when simply switching to sandbox conversations.\n- Trigger fallback toast only when a real direct-expiry transition is detected.\n\n4) Runtime guidance/notify display consistency\n- Unify live polling rendering behavior with history replay behavior.\n- Restore real-time display of guidance/notify/sub-agent/background-command messages.\n- Keep runtime_mode_notice hidden only in idle state as requested.\n\n5) Backend runtime notice emission policy\n- Suppress runtime mode notice insertion when no task is running (idle path).\n- Preserve notice injection when task is actively running.\n\n6) Tool-call ordering safety for injected completion notices\n- Delay inline sub-agent/background-command completion message insertion until all parallel tool calls in the same assistant turn are finished.\n- Prevent invalid assistant/tool_call ordering that causes provider 400 errors for missing tool_call_id responses.\n\n7) Left panel scrolling behavior\n- Enable vertical scrolling for project files / todo / sub-agent / background-command panels.\n- Hide panel scrollbars while preserving scroll capability.\n\n8) Tavily multi-key selection support\n- Introduce config/search.py with selectable env variable name for Tavily key resolution.\n- Keep existing AGENT_TAVILY_API_KEY naming compatible.\n- Export search config through config package.\n\n9) Dedicated conversation-title model config\n- Add AGENT_TITLE_API_BASE_URL / AGENT_TITLE_API_KEY / AGENT_TITLE_MODEL_ID.\n- Route title generation calls to these dedicated credentials/model with fallback defaults.\n\n10) Supporting updates\n- Update .env.example to document new Tavily and title-generation env vars.\n- Include current custom model profile tweak (kimi-k2.6).\n\nValidated with:\n- npm run build\n- python3 -m py_compile (affected backend/config modules)\n- python3 -m unittest test.test_server_refactor_smoke
This commit is contained in:
parent
e2bbd767f7
commit
0f4cf1078a
@ -9,8 +9,16 @@ AGENT_THINKING_API_BASE_URL=
|
||||
AGENT_THINKING_API_KEY=
|
||||
AGENT_THINKING_MODEL_ID=
|
||||
|
||||
# 对话标题生成模型(可选,留空则回退到基础推理模型)
|
||||
AGENT_TITLE_API_BASE_URL=
|
||||
AGENT_TITLE_API_KEY=
|
||||
AGENT_TITLE_MODEL_ID=
|
||||
|
||||
# 第三方搜索(可选)
|
||||
AGENT_TAVILY_API_KEY=
|
||||
AGENT_TAVILY_API_KEY_2=
|
||||
AGENT_TAVILY_API_KEY_BACKUP=
|
||||
# 使用哪个 Tavily 环境变量请在 config/search.py 里改 TAVILY_API_KEY_ENV_NAME
|
||||
|
||||
# 每轮最大响应 token(可选)
|
||||
AGENT_DEFAULT_RESPONSE_MAX_TOKENS=32768
|
||||
|
||||
@ -35,6 +35,7 @@ def _load_dotenv():
|
||||
_load_dotenv()
|
||||
|
||||
from . import api as _api
|
||||
from . import search as _search
|
||||
from . import paths as _paths
|
||||
from . import limits as _limits
|
||||
from . import terminal as _terminal
|
||||
@ -51,6 +52,7 @@ from . import custom_tools as _custom_tools
|
||||
from . import mcp as _mcp
|
||||
|
||||
from .api import *
|
||||
from .search import *
|
||||
from .paths import *
|
||||
from .limits import *
|
||||
from .terminal import *
|
||||
@ -67,7 +69,7 @@ from .custom_tools import *
|
||||
from .mcp import *
|
||||
|
||||
__all__ = []
|
||||
for module in (_api, _paths, _limits, _terminal, _conversation, _security, _ui, _memory, _ocr, _todo, _auth, _uploads, _sub_agent, _custom_tools, _mcp):
|
||||
for module in (_api, _search, _paths, _limits, _terminal, _conversation, _security, _ui, _memory, _ocr, _todo, _auth, _uploads, _sub_agent, _custom_tools, _mcp):
|
||||
__all__ += getattr(module, "__all__", [])
|
||||
|
||||
del _api, _paths, _limits, _terminal, _conversation, _security, _ui, _memory, _ocr, _todo, _auth, _uploads, _sub_agent, _custom_tools, _mcp
|
||||
del _api, _search, _paths, _limits, _terminal, _conversation, _security, _ui, _memory, _ocr, _todo, _auth, _uploads, _sub_agent, _custom_tools, _mcp
|
||||
|
||||
@ -19,8 +19,12 @@ THINKING_API_BASE_URL = _env("AGENT_THINKING_API_BASE_URL", API_BASE_URL)
|
||||
THINKING_API_KEY = _env("AGENT_THINKING_API_KEY", API_KEY)
|
||||
THINKING_MODEL_ID = _env("AGENT_THINKING_MODEL_ID", MODEL_ID)
|
||||
|
||||
# Tavily 搜索(可选)
|
||||
TAVILY_API_KEY = _env("AGENT_TAVILY_API_KEY", "")
|
||||
from .search import TAVILY_API_KEY
|
||||
|
||||
# 对话标题生成模型(可选,留空则回退到基础快速模型)
|
||||
TITLE_API_BASE_URL = _env("AGENT_TITLE_API_BASE_URL", API_BASE_URL)
|
||||
TITLE_API_KEY = _env("AGENT_TITLE_API_KEY", API_KEY)
|
||||
TITLE_MODEL_ID = _env("AGENT_TITLE_MODEL_ID", MODEL_ID)
|
||||
|
||||
# 默认响应 token 限制
|
||||
DEFAULT_RESPONSE_MAX_TOKENS = int(os.environ.get("AGENT_DEFAULT_RESPONSE_MAX_TOKENS", "32768"))
|
||||
@ -29,9 +33,11 @@ __all__ = [
|
||||
"API_BASE_URL",
|
||||
"API_KEY",
|
||||
"MODEL_ID",
|
||||
"TAVILY_API_KEY",
|
||||
"DEFAULT_RESPONSE_MAX_TOKENS",
|
||||
"THINKING_API_BASE_URL",
|
||||
"THINKING_API_KEY",
|
||||
"THINKING_MODEL_ID",
|
||||
"TITLE_API_BASE_URL",
|
||||
"TITLE_API_KEY",
|
||||
"TITLE_MODEL_ID",
|
||||
]
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
"max_output_tokens": 32768,
|
||||
"thinkmode_status": {
|
||||
"type": "param_toggle",
|
||||
"model_id": "kimi-k2.5",
|
||||
"model_id": "kimi-k2.6",
|
||||
"fast_extra_parameter": {
|
||||
"thinking": {
|
||||
"type": "disabled"
|
||||
|
||||
17
config/search.py
Normal file
17
config/search.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""搜索相关配置。"""
|
||||
|
||||
import os
|
||||
|
||||
# 选择 Tavily 使用哪个环境变量中的密钥。
|
||||
# 默认保持兼容:仍使用 AGENT_TAVILY_API_KEY。
|
||||
# 你可以改成例如:AGENT_TAVILY_API_KEY_2 / AGENT_TAVILY_API_KEY_BACKUP
|
||||
TAVILY_API_KEY_ENV_NAME = "AGENT_TAVILY_API_KEY_2"
|
||||
|
||||
# 实际生效的 Tavily 密钥
|
||||
TAVILY_API_KEY = os.environ.get(TAVILY_API_KEY_ENV_NAME, "")
|
||||
|
||||
__all__ = [
|
||||
"TAVILY_API_KEY_ENV_NAME",
|
||||
"TAVILY_API_KEY",
|
||||
]
|
||||
|
||||
@ -81,27 +81,8 @@ def _dispatch_runtime_mode_notice(terminal: WebTerminal, username: str, text: st
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 无运行任务时,直接写入当前会话并广播前端显示(来源固定为 notify)。
|
||||
try:
|
||||
if getattr(terminal, "context_manager", None):
|
||||
terminal.context_manager.add_conversation("user", notice, metadata=metadata)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
socketio.emit(
|
||||
"user_message",
|
||||
{
|
||||
"message": notice,
|
||||
"runtime_guidance": True,
|
||||
"runtime_guidance_original": notice,
|
||||
"runtime_mode_notice": True,
|
||||
"message_source": "notify",
|
||||
"conversation_id": getattr(getattr(terminal, "context_manager", None), "current_conversation_id", None),
|
||||
},
|
||||
room=f"user_{username}",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# 无运行任务时静默处理:不写入对话,不向前端插入通知消息。
|
||||
return
|
||||
|
||||
@chat_bp.route('/api/thinking-mode', methods=['POST'])
|
||||
@api_login_required
|
||||
|
||||
@ -5,6 +5,7 @@ import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from config import TITLE_API_BASE_URL, TITLE_API_KEY, TITLE_MODEL_ID
|
||||
from core.web_terminal import WebTerminal
|
||||
from utils.api_client import DeepSeekClient
|
||||
|
||||
@ -19,6 +20,15 @@ async def _generate_title_async(
|
||||
return None
|
||||
|
||||
client = DeepSeekClient(thinking_mode=False, web_mode=True)
|
||||
# 对话标题生成使用独立模型配置(可在 .env 中单独指定)
|
||||
client.fast_api_config = {
|
||||
"base_url": TITLE_API_BASE_URL,
|
||||
"api_key": TITLE_API_KEY,
|
||||
"model_id": TITLE_MODEL_ID,
|
||||
}
|
||||
client.api_base_url = TITLE_API_BASE_URL
|
||||
client.api_key = TITLE_API_KEY
|
||||
client.model_id = TITLE_MODEL_ID
|
||||
try:
|
||||
prompt_text = Path(title_prompt_path).read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
|
||||
@ -264,6 +264,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
||||
|
||||
# 执行每个工具
|
||||
pending_runtime_mode_notices: List[str] = []
|
||||
last_completed_tool_call_id: Optional[str] = None
|
||||
for tool_call in tool_calls:
|
||||
# 检查停止标志
|
||||
client_stop_info = get_stop_flag(client_sid, username)
|
||||
@ -1168,6 +1169,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
||||
"name": function_name,
|
||||
"content": tool_message_content
|
||||
})
|
||||
last_completed_tool_call_id = tool_call_id
|
||||
|
||||
if not pending_runtime_mode_notices:
|
||||
try:
|
||||
@ -1176,16 +1178,40 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
||||
except Exception as exc:
|
||||
debug_log(f"[RuntimeMode] 应用挂起模式变更失败: {exc}")
|
||||
|
||||
debug_log(f"[SubAgent] after tool={function_name} call_id={tool_call_id} -> poll updates")
|
||||
await process_sub_agent_updates(messages=messages, inline=True, after_tool_call_id=tool_call_id, web_terminal=web_terminal, sender=sender, debug_log=debug_log, maybe_mark_failure_from_message=maybe_mark_failure_from_message)
|
||||
debug_log(f"[BgCmdDebug] after tool={function_name} call_id={tool_call_id} -> poll background command updates (inline)")
|
||||
await process_background_command_updates(messages=messages, inline=True, after_tool_call_id=tool_call_id, web_terminal=web_terminal, sender=sender, debug_log=debug_log, maybe_mark_failure_from_message=maybe_mark_failure_from_message)
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
if tool_failed:
|
||||
mark_force_thinking(web_terminal, reason=f"{function_name}_failed")
|
||||
|
||||
# 子智能体/后台指令完成通知:必须等待同一轮全部 tool_call 都完成后再注入,
|
||||
# 避免在 assistant.tool_calls 与未完成的 tool 结果之间插入 system 消息导致 API 报错。
|
||||
if last_completed_tool_call_id:
|
||||
debug_log(
|
||||
f"[SubAgent] after all tools finished -> poll updates inline after_tool_call_id={last_completed_tool_call_id}"
|
||||
)
|
||||
await process_sub_agent_updates(
|
||||
messages=messages,
|
||||
inline=True,
|
||||
after_tool_call_id=last_completed_tool_call_id,
|
||||
web_terminal=web_terminal,
|
||||
sender=sender,
|
||||
debug_log=debug_log,
|
||||
maybe_mark_failure_from_message=maybe_mark_failure_from_message,
|
||||
)
|
||||
debug_log(
|
||||
"[BgCmdDebug] after all tools finished -> poll background command updates "
|
||||
f"inline after_tool_call_id={last_completed_tool_call_id}"
|
||||
)
|
||||
await process_background_command_updates(
|
||||
messages=messages,
|
||||
inline=True,
|
||||
after_tool_call_id=last_completed_tool_call_id,
|
||||
web_terminal=web_terminal,
|
||||
sender=sender,
|
||||
debug_log=debug_log,
|
||||
maybe_mark_failure_from_message=maybe_mark_failure_from_message,
|
||||
)
|
||||
|
||||
# 运行期模式通知:必须等待同一轮全部 tool_call 都完成后再注入,
|
||||
# 避免在 assistant.tool_calls 与对应 tool 消息之间插入 system 消息导致 API 报错。
|
||||
if pending_runtime_mode_notices:
|
||||
|
||||
@ -160,12 +160,36 @@ export const resourceMethods = {
|
||||
this.executionModeDirectUntil = status.execution_mode.direct_until ?? null;
|
||||
this.executionModeEnabled =
|
||||
typeof status.execution_mode_enabled === 'boolean' ? status.execution_mode_enabled : true;
|
||||
if (typeof this.scheduleExecutionModeExpirySync === 'function') {
|
||||
this.scheduleExecutionModeExpirySync(this.executionModeDirectUntil);
|
||||
}
|
||||
const currentMode = this.currentExecutionMode;
|
||||
const parseDirectUntilMs = (value: any): number => {
|
||||
if (!value) return 0;
|
||||
if (typeof value === 'number') {
|
||||
return value > 1e12 ? value : value * 1000;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Date.parse(value);
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
const prevDirectUntilMs = parseDirectUntilMs(prevDirectUntil);
|
||||
const fallbackLikelyByExpiry = !!prevDirectUntilMs && Date.now() >= prevDirectUntilMs - 1200;
|
||||
const directExpiredToSandbox =
|
||||
prevExecutionMode === 'direct' &&
|
||||
currentMode === 'sandbox' &&
|
||||
!!prevDirectUntil;
|
||||
prevExecutionMode === 'direct' && currentMode === 'sandbox' && fallbackLikelyByExpiry;
|
||||
if (directExpiredToSandbox) {
|
||||
if (this.currentPermissionMode === 'unrestricted') {
|
||||
if (typeof pendingModes.permission_mode === 'string' && pendingModes.permission_mode) {
|
||||
this.currentPermissionMode = pendingModes.permission_mode;
|
||||
} else {
|
||||
this.currentPermissionMode = 'approval';
|
||||
}
|
||||
}
|
||||
if (typeof this.fetchPermissionMode === 'function') {
|
||||
Promise.resolve(this.fetchPermissionMode()).catch(() => {});
|
||||
}
|
||||
try {
|
||||
if (this.executionModeAutoFallbackToastId) {
|
||||
this.uiDismissToast(this.executionModeAutoFallbackToastId);
|
||||
@ -173,7 +197,7 @@ export const resourceMethods = {
|
||||
} catch (_err) {}
|
||||
this.executionModeAutoFallbackToastId = this.uiPushToast({
|
||||
title: '执行环境已自动回退',
|
||||
message: '完全访问权限已到期,当前已切换为沙箱执行环境。',
|
||||
message: '完全访问权限已到期,已自动切回受限权限并切换为沙箱执行环境。',
|
||||
type: 'warning',
|
||||
duration: null
|
||||
});
|
||||
|
||||
@ -84,12 +84,7 @@ function isRuntimeModeNoticePayload(data: any): boolean {
|
||||
return false;
|
||||
}
|
||||
const meta = data.metadata || {};
|
||||
return !!(
|
||||
data.runtime_guidance ||
|
||||
meta.runtime_guidance ||
|
||||
data.runtime_mode_notice ||
|
||||
meta.runtime_mode_notice
|
||||
);
|
||||
return !!(data.runtime_mode_notice || meta.runtime_mode_notice);
|
||||
}
|
||||
|
||||
function resolveUserMessageSource(data: any): string {
|
||||
@ -1582,13 +1577,17 @@ export const taskPollingMethods = {
|
||||
this.taskInProgress = true;
|
||||
this.streamingMessage = false;
|
||||
this.stopRequested = false;
|
||||
} else if (isRuntimeModeNotice) {
|
||||
this.chatAddUserMessage(message, [], [], [], 'notify');
|
||||
} else {
|
||||
debugLog('[TaskPolling] 跳过自动代发 user_message 渲染', {
|
||||
messagePreview: message.slice(0, 80),
|
||||
data
|
||||
});
|
||||
const source = resolveUserMessageSource(data);
|
||||
// 仅在对话运行期间展示 runtime_mode_notice;其余自动消息按来源正常显示,
|
||||
// 保持与后端历史重建一致(guidance/notify/sub_agent/background_command)。
|
||||
if (isRuntimeModeNotice && !(this.taskInProgress || this.streamingMessage)) {
|
||||
debugLog('[TaskPolling] 空闲态跳过 runtime_mode_notice 用户通知', {
|
||||
messagePreview: message.slice(0, 80)
|
||||
});
|
||||
} else {
|
||||
this.chatAddUserMessage(message, [], [], [], source);
|
||||
}
|
||||
}
|
||||
if (data?.sub_agent_notice) {
|
||||
if (typeof data?.has_running_sub_agents === 'boolean') {
|
||||
|
||||
@ -1204,6 +1204,7 @@ export const uiMethods = {
|
||||
this.executionModeDirectUntil = state.direct_until ?? null;
|
||||
this.executionModeTtlSeconds =
|
||||
typeof state.ttl_seconds === 'number' ? state.ttl_seconds : this.executionModeTtlSeconds;
|
||||
this.scheduleExecutionModeExpirySync(this.executionModeDirectUntil);
|
||||
this.uiPushToast({
|
||||
title: '执行环境已更新',
|
||||
message: payload?.message || '已立即生效',
|
||||
@ -1245,6 +1246,8 @@ export const uiMethods = {
|
||||
|
||||
async fetchExecutionMode() {
|
||||
try {
|
||||
const prevExecutionMode = this.currentExecutionMode;
|
||||
const prevDirectUntil = this.executionModeDirectUntil;
|
||||
const response = await fetch('/api/execution-mode');
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !payload?.success) {
|
||||
@ -1260,11 +1263,78 @@ export const uiMethods = {
|
||||
if (typeof state.ttl_seconds === 'number') {
|
||||
this.executionModeTtlSeconds = state.ttl_seconds;
|
||||
}
|
||||
this.scheduleExecutionModeExpirySync(this.executionModeDirectUntil);
|
||||
const parseDirectUntilMs = (value) => {
|
||||
if (!value) return 0;
|
||||
if (typeof value === 'number') {
|
||||
return value > 1e12 ? value : value * 1000;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Date.parse(value);
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
const prevDirectUntilMs = parseDirectUntilMs(prevDirectUntil);
|
||||
const fallbackLikelyByExpiry = !!prevDirectUntilMs && Date.now() >= prevDirectUntilMs - 1200;
|
||||
const directExpiredToSandbox =
|
||||
prevExecutionMode === 'direct' &&
|
||||
this.currentExecutionMode === 'sandbox' &&
|
||||
fallbackLikelyByExpiry;
|
||||
if (directExpiredToSandbox) {
|
||||
if (this.currentPermissionMode === 'unrestricted') {
|
||||
this.currentPermissionMode = 'approval';
|
||||
}
|
||||
Promise.resolve(this.fetchPermissionMode()).catch(() => {});
|
||||
try {
|
||||
if (this.executionModeAutoFallbackToastId) {
|
||||
this.uiDismissToast(this.executionModeAutoFallbackToastId);
|
||||
}
|
||||
} catch (_err) {}
|
||||
this.executionModeAutoFallbackToastId = this.uiPushToast({
|
||||
title: '执行环境已自动回退',
|
||||
message: '完全访问权限已到期,已自动切回受限权限并切换为沙箱执行环境。',
|
||||
type: 'warning',
|
||||
duration: null
|
||||
});
|
||||
}
|
||||
} catch (_error) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
clearExecutionModeExpirySyncTimer() {
|
||||
if (this.executionModeExpirySyncTimer) {
|
||||
clearTimeout(this.executionModeExpirySyncTimer);
|
||||
this.executionModeExpirySyncTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
scheduleExecutionModeExpirySync(directUntil) {
|
||||
this.clearExecutionModeExpirySyncTimer();
|
||||
if (!this.executionModeEnabled || this.currentExecutionMode !== 'direct' || !directUntil) {
|
||||
return;
|
||||
}
|
||||
let untilMs = 0;
|
||||
if (typeof directUntil === 'number') {
|
||||
untilMs = directUntil > 1e12 ? directUntil : directUntil * 1000;
|
||||
} else if (typeof directUntil === 'string') {
|
||||
const parsed = Date.parse(directUntil);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
untilMs = parsed;
|
||||
}
|
||||
}
|
||||
if (!untilMs) {
|
||||
return;
|
||||
}
|
||||
const delayMs = Math.max(250, untilMs - Date.now() + 250);
|
||||
this.executionModeExpirySyncTimer = window.setTimeout(async () => {
|
||||
this.executionModeExpirySyncTimer = null;
|
||||
await this.fetchExecutionMode();
|
||||
await this.fetchPermissionMode();
|
||||
}, delayMs);
|
||||
},
|
||||
|
||||
async openPathAuthorizationDialog() {
|
||||
if (!this.executionModeEnabled) return;
|
||||
this.pathAuthorizationDialogOpen = true;
|
||||
|
||||
@ -104,6 +104,7 @@ export function dataState() {
|
||||
executionModeDirectUntil: null,
|
||||
executionModeAutoFallbackToastId: null,
|
||||
executionModeTtlSeconds: 600,
|
||||
executionModeExpirySyncTimer: null,
|
||||
pathAuthorizationDialogOpen: false,
|
||||
pathAuthorizationMode: 'writable',
|
||||
pathAuthorizationWritableDraft: '',
|
||||
|
||||
@ -338,11 +338,32 @@ function normalizeShowTagsPlugin() {
|
||||
};
|
||||
}
|
||||
|
||||
function wrapMarkdownTablesPlugin() {
|
||||
return (tree: any) => {
|
||||
visit(tree, 'element', (node: any, index: number | undefined, parent: any) => {
|
||||
if (!node || node.tagName !== 'table' || !parent || typeof index !== 'number') return;
|
||||
if (parent?.tagName === 'div' && Array.isArray(parent?.properties?.className)) {
|
||||
const classes = parent.properties.className as string[];
|
||||
if (classes.includes('md-table-scroll')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
parent.children[index] = {
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: { className: ['md-table-scroll'], 'data-md-table-scroll': '1' },
|
||||
children: [node]
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const sanitizedSchema: Record<string, any> = {
|
||||
...defaultSchema,
|
||||
tagNames: [...(defaultSchema.tagNames || []), 'show_html', 'show_image', 'show-html', 'show-image'],
|
||||
attributes: {
|
||||
...(defaultSchema.attributes || {}),
|
||||
div: [...((defaultSchema.attributes || {}).div || []), 'className', 'data-md-table-scroll'],
|
||||
show_html: ['ratio', 'js', 'data-encoded', 'data-partial'],
|
||||
show_image: ['src', 'alt', 'title', 'width', 'height'],
|
||||
'show-html': ['ratio', 'js', 'data-encoded', 'data-partial'],
|
||||
@ -357,6 +378,7 @@ const markdownProcessor = unified()
|
||||
.use(remarkRehype, { allowDangerousHtml: true })
|
||||
.use(rehypeRaw)
|
||||
.use(normalizeShowTagsPlugin)
|
||||
.use(wrapMarkdownTablesPlugin)
|
||||
.use(rehypeSanitize, sanitizedSchema)
|
||||
.use(rehypeStringify, { allowDangerousHtml: false });
|
||||
|
||||
|
||||
@ -74,6 +74,7 @@
|
||||
.messages-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-anchor: none;
|
||||
padding: 24px;
|
||||
padding-top: 30px;
|
||||
@ -412,6 +413,7 @@
|
||||
|
||||
.message-block {
|
||||
margin-bottom: 24px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.message-block.message-block--sub-agent-notice {
|
||||
@ -1084,6 +1086,9 @@ show-html:not([data-rendered="1"])[ratio="3:4"] { aspect-ratio: 3 / 4; }
|
||||
|
||||
.text-output .text-content {
|
||||
padding: 0 20px 0 15px;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.text-output pre {
|
||||
@ -1093,7 +1098,8 @@ show-html:not([data-rendered="1"])[ratio="3:4"] { aspect-ratio: 3 / 4; }
|
||||
line-height: 1.55;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
scrollbar-gutter: stable;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.text-output pre code {
|
||||
@ -1106,15 +1112,58 @@ show-html:not([data-rendered="1"])[ratio="3:4"] { aspect-ratio: 3 / 4; }
|
||||
}
|
||||
|
||||
.text-output .text-content table {
|
||||
width: 100%;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
margin: 0;
|
||||
background: var(--theme-surface-soft);
|
||||
}
|
||||
|
||||
.text-output .text-content .md-table-scroll,
|
||||
.text-output .text-content [data-md-table-scroll='1'] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 16px 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
border: 1px solid var(--claude-border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: var(--theme-surface-soft);
|
||||
box-shadow: var(--theme-shadow-soft);
|
||||
}
|
||||
|
||||
.text-output .text-content .md-table-scroll > table,
|
||||
.text-output .text-content [data-md-table-scroll='1'] > table {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.text-output .text-content .md-table-scroll::-webkit-scrollbar,
|
||||
.text-output .text-content [data-md-table-scroll='1']::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.text-output .text-content .md-table-scroll::-webkit-scrollbar-track,
|
||||
.text-output .text-content [data-md-table-scroll='1']::-webkit-scrollbar-track {
|
||||
background: color-mix(in srgb, var(--theme-surface-muted, #d1d5db) 70%, transparent);
|
||||
border-radius: 999px;
|
||||
margin: 0 8px 6px;
|
||||
}
|
||||
|
||||
.text-output .text-content .md-table-scroll::-webkit-scrollbar-thumb,
|
||||
.text-output .text-content [data-md-table-scroll='1']::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--claude-text-secondary, #6b7280) 55%, transparent);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.text-output .text-content .md-table-scroll,
|
||||
.text-output .text-content [data-md-table-scroll='1'] {
|
||||
scrollbar-color: color-mix(in srgb, var(--claude-text-secondary, #6b7280) 55%, transparent)
|
||||
color-mix(in srgb, var(--theme-surface-muted, #d1d5db) 70%, transparent);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.text-output .text-content thead {
|
||||
background: var(--theme-chip-bg);
|
||||
}
|
||||
@ -1251,7 +1300,8 @@ show-html:not([data-rendered="1"])[ratio="3:4"] { aspect-ratio: 3 / 4; }
|
||||
line-height: 1.55;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
scrollbar-gutter: stable;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.code-block-wrapper pre code {
|
||||
|
||||
@ -396,10 +396,13 @@
|
||||
}
|
||||
|
||||
.todo-panel {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 16px 20px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sub-agent-panel {
|
||||
@ -412,6 +415,22 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 左侧四个工作面板统一支持纵向滚动 */
|
||||
.file-tree,
|
||||
.todo-panel,
|
||||
.sub-agent-panel {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.file-tree::-webkit-scrollbar,
|
||||
.todo-panel::-webkit-scrollbar,
|
||||
.sub-agent-panel::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.sub-agent-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user