diff --git a/.env.example b/.env.example index 243a0c7..3ff2bcd 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/config/__init__.py b/config/__init__.py index cbd9467..5443ee3 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -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 diff --git a/config/api.py b/config/api.py index d169cac..eeb86c6 100644 --- a/config/api.py +++ b/config/api.py @@ -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", ] diff --git a/config/custom_models.json b/config/custom_models.json index 4c7eb71..8b55112 100644 --- a/config/custom_models.json +++ b/config/custom_models.json @@ -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" diff --git a/config/search.py b/config/search.py new file mode 100644 index 0000000..ac30499 --- /dev/null +++ b/config/search.py @@ -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", +] + diff --git a/server/chat.py b/server/chat.py index 4bc0157..91fd80b 100644 --- a/server/chat.py +++ b/server/chat.py @@ -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 diff --git a/server/chat_flow_helpers.py b/server/chat_flow_helpers.py index b68e1f4..b44e057 100644 --- a/server/chat_flow_helpers.py +++ b/server/chat_flow_helpers.py @@ -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: diff --git a/server/chat_flow_tool_loop.py b/server/chat_flow_tool_loop.py index 5319be1..581a358 100644 --- a/server/chat_flow_tool_loop.py +++ b/server/chat_flow_tool_loop.py @@ -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: diff --git a/static/src/app/methods/resources.ts b/static/src/app/methods/resources.ts index 96247c2..5b8526f 100644 --- a/static/src/app/methods/resources.ts +++ b/static/src/app/methods/resources.ts @@ -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 }); diff --git a/static/src/app/methods/taskPolling.ts b/static/src/app/methods/taskPolling.ts index ba49bc6..20f45f3 100644 --- a/static/src/app/methods/taskPolling.ts +++ b/static/src/app/methods/taskPolling.ts @@ -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') { diff --git a/static/src/app/methods/ui.ts b/static/src/app/methods/ui.ts index 94b27b4..5e42570 100644 --- a/static/src/app/methods/ui.ts +++ b/static/src/app/methods/ui.ts @@ -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; diff --git a/static/src/app/state.ts b/static/src/app/state.ts index b951795..d545a24 100644 --- a/static/src/app/state.ts +++ b/static/src/app/state.ts @@ -104,6 +104,7 @@ export function dataState() { executionModeDirectUntil: null, executionModeAutoFallbackToastId: null, executionModeTtlSeconds: 600, + executionModeExpirySyncTimer: null, pathAuthorizationDialogOpen: false, pathAuthorizationMode: 'writable', pathAuthorizationWritableDraft: '', diff --git a/static/src/composables/useMarkdownRenderer.ts b/static/src/composables/useMarkdownRenderer.ts index 64c1037..0e0017f 100644 --- a/static/src/composables/useMarkdownRenderer.ts +++ b/static/src/composables/useMarkdownRenderer.ts @@ -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 = { ...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 }); diff --git a/static/src/styles/components/chat/_chat-area.scss b/static/src/styles/components/chat/_chat-area.scss index 75b7146..502d52c 100644 --- a/static/src/styles/components/chat/_chat-area.scss +++ b/static/src/styles/components/chat/_chat-area.scss @@ -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 { diff --git a/static/src/styles/components/panels/_left-panel.scss b/static/src/styles/components/panels/_left-panel.scss index b802f3f..ddf2e50 100644 --- a/static/src/styles/components/panels/_left-panel.scss +++ b/static/src/styles/components/panels/_left-panel.scss @@ -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;