Compare commits

...

1 Commits

Author SHA1 Message Date
0f4cf1078a 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
2026-05-13 15:13:52 +08:00
15 changed files with 288 additions and 53 deletions

View File

@ -9,8 +9,16 @@ AGENT_THINKING_API_BASE_URL=
AGENT_THINKING_API_KEY= AGENT_THINKING_API_KEY=
AGENT_THINKING_MODEL_ID= 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=
AGENT_TAVILY_API_KEY_2=
AGENT_TAVILY_API_KEY_BACKUP=
# 使用哪个 Tavily 环境变量请在 config/search.py 里改 TAVILY_API_KEY_ENV_NAME
# 每轮最大响应 token可选 # 每轮最大响应 token可选
AGENT_DEFAULT_RESPONSE_MAX_TOKENS=32768 AGENT_DEFAULT_RESPONSE_MAX_TOKENS=32768

View File

@ -35,6 +35,7 @@ def _load_dotenv():
_load_dotenv() _load_dotenv()
from . import api as _api from . import api as _api
from . import search as _search
from . import paths as _paths from . import paths as _paths
from . import limits as _limits from . import limits as _limits
from . import terminal as _terminal from . import terminal as _terminal
@ -51,6 +52,7 @@ from . import custom_tools as _custom_tools
from . import mcp as _mcp from . import mcp as _mcp
from .api import * from .api import *
from .search import *
from .paths import * from .paths import *
from .limits import * from .limits import *
from .terminal import * from .terminal import *
@ -67,7 +69,7 @@ from .custom_tools import *
from .mcp import * from .mcp import *
__all__ = [] __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__", []) __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

View File

@ -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_API_KEY = _env("AGENT_THINKING_API_KEY", API_KEY)
THINKING_MODEL_ID = _env("AGENT_THINKING_MODEL_ID", MODEL_ID) THINKING_MODEL_ID = _env("AGENT_THINKING_MODEL_ID", MODEL_ID)
# Tavily 搜索(可选) from .search import TAVILY_API_KEY
TAVILY_API_KEY = _env("AGENT_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 限制 # 默认响应 token 限制
DEFAULT_RESPONSE_MAX_TOKENS = int(os.environ.get("AGENT_DEFAULT_RESPONSE_MAX_TOKENS", "32768")) DEFAULT_RESPONSE_MAX_TOKENS = int(os.environ.get("AGENT_DEFAULT_RESPONSE_MAX_TOKENS", "32768"))
@ -29,9 +33,11 @@ __all__ = [
"API_BASE_URL", "API_BASE_URL",
"API_KEY", "API_KEY",
"MODEL_ID", "MODEL_ID",
"TAVILY_API_KEY",
"DEFAULT_RESPONSE_MAX_TOKENS", "DEFAULT_RESPONSE_MAX_TOKENS",
"THINKING_API_BASE_URL", "THINKING_API_BASE_URL",
"THINKING_API_KEY", "THINKING_API_KEY",
"THINKING_MODEL_ID", "THINKING_MODEL_ID",
"TITLE_API_BASE_URL",
"TITLE_API_KEY",
"TITLE_MODEL_ID",
] ]

View File

@ -12,7 +12,7 @@
"max_output_tokens": 32768, "max_output_tokens": 32768,
"thinkmode_status": { "thinkmode_status": {
"type": "param_toggle", "type": "param_toggle",
"model_id": "kimi-k2.5", "model_id": "kimi-k2.6",
"fast_extra_parameter": { "fast_extra_parameter": {
"thinking": { "thinking": {
"type": "disabled" "type": "disabled"

17
config/search.py Normal file
View 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",
]

View File

@ -81,27 +81,8 @@ def _dispatch_runtime_mode_notice(terminal: WebTerminal, username: str, text: st
except Exception: except Exception:
pass pass
# 无运行任务时,直接写入当前会话并广播前端显示(来源固定为 notify # 无运行任务时静默处理:不写入对话,不向前端插入通知消息。
try: return
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
@chat_bp.route('/api/thinking-mode', methods=['POST']) @chat_bp.route('/api/thinking-mode', methods=['POST'])
@api_login_required @api_login_required

View File

@ -5,6 +5,7 @@ import re
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional 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 core.web_terminal import WebTerminal
from utils.api_client import DeepSeekClient from utils.api_client import DeepSeekClient
@ -19,6 +20,15 @@ async def _generate_title_async(
return None return None
client = DeepSeekClient(thinking_mode=False, web_mode=True) 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: try:
prompt_text = Path(title_prompt_path).read_text(encoding="utf-8") prompt_text = Path(title_prompt_path).read_text(encoding="utf-8")
except Exception: except Exception:

View File

@ -264,6 +264,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
# 执行每个工具 # 执行每个工具
pending_runtime_mode_notices: List[str] = [] pending_runtime_mode_notices: List[str] = []
last_completed_tool_call_id: Optional[str] = None
for tool_call in tool_calls: for tool_call in tool_calls:
# 检查停止标志 # 检查停止标志
client_stop_info = get_stop_flag(client_sid, username) 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, "name": function_name,
"content": tool_message_content "content": tool_message_content
}) })
last_completed_tool_call_id = tool_call_id
if not pending_runtime_mode_notices: if not pending_runtime_mode_notices:
try: try:
@ -1176,16 +1178,40 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
except Exception as exc: except Exception as exc:
debug_log(f"[RuntimeMode] 应用挂起模式变更失败: {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) await asyncio.sleep(0.2)
if tool_failed: if tool_failed:
mark_force_thinking(web_terminal, reason=f"{function_name}_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 都完成后再注入, # 运行期模式通知:必须等待同一轮全部 tool_call 都完成后再注入,
# 避免在 assistant.tool_calls 与对应 tool 消息之间插入 system 消息导致 API 报错。 # 避免在 assistant.tool_calls 与对应 tool 消息之间插入 system 消息导致 API 报错。
if pending_runtime_mode_notices: if pending_runtime_mode_notices:

View File

@ -160,12 +160,36 @@ export const resourceMethods = {
this.executionModeDirectUntil = status.execution_mode.direct_until ?? null; this.executionModeDirectUntil = status.execution_mode.direct_until ?? null;
this.executionModeEnabled = this.executionModeEnabled =
typeof status.execution_mode_enabled === 'boolean' ? status.execution_mode_enabled : true; 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 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 = const directExpiredToSandbox =
prevExecutionMode === 'direct' && prevExecutionMode === 'direct' && currentMode === 'sandbox' && fallbackLikelyByExpiry;
currentMode === 'sandbox' &&
!!prevDirectUntil;
if (directExpiredToSandbox) { 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 { try {
if (this.executionModeAutoFallbackToastId) { if (this.executionModeAutoFallbackToastId) {
this.uiDismissToast(this.executionModeAutoFallbackToastId); this.uiDismissToast(this.executionModeAutoFallbackToastId);
@ -173,7 +197,7 @@ export const resourceMethods = {
} catch (_err) {} } catch (_err) {}
this.executionModeAutoFallbackToastId = this.uiPushToast({ this.executionModeAutoFallbackToastId = this.uiPushToast({
title: '执行环境已自动回退', title: '执行环境已自动回退',
message: '完全访问权限已到期,当前已切换为沙箱执行环境。', message: '完全访问权限已到期,自动切回受限权限并切换为沙箱执行环境。',
type: 'warning', type: 'warning',
duration: null duration: null
}); });

View File

@ -84,12 +84,7 @@ function isRuntimeModeNoticePayload(data: any): boolean {
return false; return false;
} }
const meta = data.metadata || {}; const meta = data.metadata || {};
return !!( return !!(data.runtime_mode_notice || meta.runtime_mode_notice);
data.runtime_guidance ||
meta.runtime_guidance ||
data.runtime_mode_notice ||
meta.runtime_mode_notice
);
} }
function resolveUserMessageSource(data: any): string { function resolveUserMessageSource(data: any): string {
@ -1582,13 +1577,17 @@ export const taskPollingMethods = {
this.taskInProgress = true; this.taskInProgress = true;
this.streamingMessage = false; this.streamingMessage = false;
this.stopRequested = false; this.stopRequested = false;
} else if (isRuntimeModeNotice) {
this.chatAddUserMessage(message, [], [], [], 'notify');
} else { } else {
debugLog('[TaskPolling] 跳过自动代发 user_message 渲染', { const source = resolveUserMessageSource(data);
messagePreview: message.slice(0, 80), // 仅在对话运行期间展示 runtime_mode_notice其余自动消息按来源正常显示
data // 保持与后端历史重建一致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 (data?.sub_agent_notice) {
if (typeof data?.has_running_sub_agents === 'boolean') { if (typeof data?.has_running_sub_agents === 'boolean') {

View File

@ -1204,6 +1204,7 @@ export const uiMethods = {
this.executionModeDirectUntil = state.direct_until ?? null; this.executionModeDirectUntil = state.direct_until ?? null;
this.executionModeTtlSeconds = this.executionModeTtlSeconds =
typeof state.ttl_seconds === 'number' ? state.ttl_seconds : this.executionModeTtlSeconds; typeof state.ttl_seconds === 'number' ? state.ttl_seconds : this.executionModeTtlSeconds;
this.scheduleExecutionModeExpirySync(this.executionModeDirectUntil);
this.uiPushToast({ this.uiPushToast({
title: '执行环境已更新', title: '执行环境已更新',
message: payload?.message || '已立即生效', message: payload?.message || '已立即生效',
@ -1245,6 +1246,8 @@ export const uiMethods = {
async fetchExecutionMode() { async fetchExecutionMode() {
try { try {
const prevExecutionMode = this.currentExecutionMode;
const prevDirectUntil = this.executionModeDirectUntil;
const response = await fetch('/api/execution-mode'); const response = await fetch('/api/execution-mode');
const payload = await response.json().catch(() => ({})); const payload = await response.json().catch(() => ({}));
if (!response.ok || !payload?.success) { if (!response.ok || !payload?.success) {
@ -1260,11 +1263,78 @@ export const uiMethods = {
if (typeof state.ttl_seconds === 'number') { if (typeof state.ttl_seconds === 'number') {
this.executionModeTtlSeconds = state.ttl_seconds; 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) { } catch (_error) {
// ignore // 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() { async openPathAuthorizationDialog() {
if (!this.executionModeEnabled) return; if (!this.executionModeEnabled) return;
this.pathAuthorizationDialogOpen = true; this.pathAuthorizationDialogOpen = true;

View File

@ -104,6 +104,7 @@ export function dataState() {
executionModeDirectUntil: null, executionModeDirectUntil: null,
executionModeAutoFallbackToastId: null, executionModeAutoFallbackToastId: null,
executionModeTtlSeconds: 600, executionModeTtlSeconds: 600,
executionModeExpirySyncTimer: null,
pathAuthorizationDialogOpen: false, pathAuthorizationDialogOpen: false,
pathAuthorizationMode: 'writable', pathAuthorizationMode: 'writable',
pathAuthorizationWritableDraft: '', pathAuthorizationWritableDraft: '',

View File

@ -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> = { const sanitizedSchema: Record<string, any> = {
...defaultSchema, ...defaultSchema,
tagNames: [...(defaultSchema.tagNames || []), 'show_html', 'show_image', 'show-html', 'show-image'], tagNames: [...(defaultSchema.tagNames || []), 'show_html', 'show_image', 'show-html', 'show-image'],
attributes: { attributes: {
...(defaultSchema.attributes || {}), ...(defaultSchema.attributes || {}),
div: [...((defaultSchema.attributes || {}).div || []), 'className', 'data-md-table-scroll'],
show_html: ['ratio', 'js', 'data-encoded', 'data-partial'], show_html: ['ratio', 'js', 'data-encoded', 'data-partial'],
show_image: ['src', 'alt', 'title', 'width', 'height'], show_image: ['src', 'alt', 'title', 'width', 'height'],
'show-html': ['ratio', 'js', 'data-encoded', 'data-partial'], 'show-html': ['ratio', 'js', 'data-encoded', 'data-partial'],
@ -357,6 +378,7 @@ const markdownProcessor = unified()
.use(remarkRehype, { allowDangerousHtml: true }) .use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw) .use(rehypeRaw)
.use(normalizeShowTagsPlugin) .use(normalizeShowTagsPlugin)
.use(wrapMarkdownTablesPlugin)
.use(rehypeSanitize, sanitizedSchema) .use(rehypeSanitize, sanitizedSchema)
.use(rehypeStringify, { allowDangerousHtml: false }); .use(rehypeStringify, { allowDangerousHtml: false });

View File

@ -74,6 +74,7 @@
.messages-area { .messages-area {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden;
overflow-anchor: none; overflow-anchor: none;
padding: 24px; padding: 24px;
padding-top: 30px; padding-top: 30px;
@ -412,6 +413,7 @@
.message-block { .message-block {
margin-bottom: 24px; margin-bottom: 24px;
min-width: 0;
} }
.message-block.message-block--sub-agent-notice { .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 { .text-output .text-content {
padding: 0 20px 0 15px; padding: 0 20px 0 15px;
max-width: 100%;
min-width: 0;
overflow-x: hidden;
} }
.text-output pre { .text-output pre {
@ -1093,7 +1098,8 @@ show-html:not([data-rendered="1"])[ratio="3:4"] { aspect-ratio: 3 / 4; }
line-height: 1.55; line-height: 1.55;
overflow-x: auto; overflow-x: auto;
max-width: 100%; max-width: 100%;
scrollbar-gutter: stable both-edges; scrollbar-gutter: stable;
box-sizing: border-box;
} }
.text-output pre code { .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 { .text-output .text-content table {
width: 100%; width: max-content;
min-width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin: 16px 0; margin: 0;
background: var(--theme-surface-soft); 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; border-radius: 12px;
overflow: hidden; background: var(--theme-surface-soft);
box-shadow: var(--theme-shadow-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 { .text-output .text-content thead {
background: var(--theme-chip-bg); 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; line-height: 1.55;
overflow-x: auto; overflow-x: auto;
max-width: 100%; max-width: 100%;
scrollbar-gutter: stable both-edges; scrollbar-gutter: stable;
box-sizing: border-box;
} }
.code-block-wrapper pre code { .code-block-wrapper pre code {

View File

@ -396,10 +396,13 @@
} }
.todo-panel { .todo-panel {
flex: 1 1 auto;
min-height: 0;
padding: 16px 20px 24px; padding: 16px 20px 24px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
overflow-y: auto;
} }
.sub-agent-panel { .sub-agent-panel {
@ -412,6 +415,22 @@
overflow-y: auto; 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 { .sub-agent-cards {
display: flex; display: flex;
flex-direction: column; flex-direction: column;