diff --git a/modules/personalization_manager.py b/modules/personalization_manager.py
index bfe2683..ffcb54e 100644
--- a/modules/personalization_manager.py
+++ b/modules/personalization_manager.py
@@ -35,6 +35,7 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
"disabled_tool_categories": [],
"default_run_mode": None,
"auto_generate_title": True,
+ "tool_intent_enabled": True,
}
__all__ = [
@@ -124,6 +125,12 @@ def sanitize_personalization_payload(
else:
base["thinking_interval"] = _sanitize_thinking_interval(base.get("thinking_interval"))
+ # 工具意图提示开关
+ if "tool_intent_enabled" in data:
+ base["tool_intent_enabled"] = bool(data.get("tool_intent_enabled"))
+ else:
+ base["tool_intent_enabled"] = bool(base.get("tool_intent_enabled"))
+
if "disabled_tool_categories" in data:
base["disabled_tool_categories"] = _sanitize_tool_categories(data.get("disabled_tool_categories"), allowed_tool_categories)
else:
diff --git a/static/src/app.ts b/static/src/app.ts
index f2796eb..0cddee6 100644
--- a/static/src/app.ts
+++ b/static/src/app.ts
@@ -45,7 +45,7 @@ import {
import {
getToolIcon,
getToolAnimationClass,
- getToolStatusText,
+ getToolStatusText as baseGetToolStatusText,
getToolDescription,
cloneToolArguments,
buildToolLabel,
@@ -1653,6 +1653,8 @@ const appOptions = {
id: toolCall.id,
name: toolCall.function.name,
arguments: arguments_obj,
+ intent_full: arguments_obj.intent || '',
+ intent_rendered: arguments_obj.intent || '',
status: 'preparing',
result: null
},
@@ -2585,7 +2587,14 @@ const appOptions = {
},
getToolIcon,
getToolAnimationClass,
- getToolStatusText,
+ getToolStatusText(tool: any) {
+ const personalization = usePersonalizationStore();
+ const intentEnabled =
+ personalization?.form?.tool_intent_enabled ??
+ personalization?.tool_intent_enabled ??
+ true;
+ return baseGetToolStatusText(tool, { intentEnabled });
+ },
getToolDescription,
cloneToolArguments,
buildToolLabel,
diff --git a/static/src/components/chat/StackedBlocks.vue b/static/src/components/chat/StackedBlocks.vue
index f5897bb..d40670f 100644
--- a/static/src/components/chat/StackedBlocks.vue
+++ b/static/src/components/chat/StackedBlocks.vue
@@ -22,7 +22,14 @@
+
@@ -235,7 +242,8 @@ const setShellMetrics = (expandedOverride: Record = {}) => {
const contentHeight = content ? Math.ceil(content.scrollHeight) : 0;
nextContentHeights[key] = contentHeight;
const contentH = expanded ? contentHeight : 0;
- const progressH = progress ? progress.getBoundingClientRect().height : 0;
+ // 进度条是绝对定位的,不应计入布局高度,否则会导致块高度在运行时膨胀几像素
+ const progressH = 0;
const borderH = idx < children.length - 1 ? parseFloat(getComputedStyle(el).borderBottomWidth) || 0 : 0;
heights.push(Math.ceil(headerH + contentH + progressH + borderH));
});
diff --git a/static/src/composables/useLegacySocket.ts b/static/src/composables/useLegacySocket.ts
index 0a8f7d3..24a4047 100644
--- a/static/src/composables/useLegacySocket.ts
+++ b/static/src/composables/useLegacySocket.ts
@@ -39,6 +39,75 @@ export async function initializeLegacySocket(ctx: any) {
const pendingToolEvents: Array<{ event: string; data: any; handler: () => void }> = [];
+ const startIntentTyping = (action: any, intentText: any) => {
+ if (!action || !action.tool) {
+ return;
+ }
+ if (typeof intentText !== 'string') {
+ return;
+ }
+ const text = intentText.trim();
+ if (!text) {
+ return;
+ }
+ if (action.tool.intent_full === text) {
+ if (console && console.debug) {
+ console.debug('[tool_intent] skip (same text)', {
+ id: action?.id || action?.tool?.id,
+ name: action?.tool?.name,
+ text
+ });
+ }
+ return;
+ }
+ if (typeof console !== 'undefined' && console.debug) {
+ console.debug('[tool_intent] typing start', {
+ id: action?.id || action?.tool?.id,
+ name: action?.tool?.name,
+ text
+ });
+ }
+ if (action.tool.intent_full === text && action.tool.intent_rendered === text) {
+ return;
+ }
+ // 清理旧定时器
+ if (action.tool.intent_typing_timer) {
+ clearInterval(action.tool.intent_typing_timer);
+ action.tool.intent_typing_timer = null;
+ }
+ action.tool.intent_full = text;
+ action.tool.intent_rendered = '';
+ const duration = 1000;
+ const step = Math.max(10, Math.floor(duration / Math.max(1, text.length)));
+ let index = 0;
+ action.tool.intent_typing_timer = window.setInterval(() => {
+ action.tool.intent_rendered = text.slice(0, index + 1);
+ if (index === 0 && console && console.debug) {
+ console.debug('[tool_intent] typing tick', {
+ id: action?.id || action?.tool?.id,
+ name: action?.tool?.name,
+ next: action.tool.intent_rendered
+ });
+ }
+ index += 1;
+ if (index >= text.length) {
+ clearInterval(action.tool.intent_typing_timer);
+ action.tool.intent_typing_timer = null;
+ action.tool.intent_rendered = text;
+ if (console && console.debug) {
+ console.debug('[tool_intent] typing done', {
+ id: action?.id || action?.tool?.id,
+ name: action?.tool?.name,
+ full: text
+ });
+ }
+ }
+ if (typeof ctx?.$forceUpdate === 'function') {
+ ctx.$forceUpdate();
+ }
+ }, step);
+ };
+
const hasPendingStreamingText = () =>
!!streamingState.buffer.length ||
!!streamingState.pendingCompleteContent ||
@@ -845,6 +914,7 @@ export async function initializeLegacySocket(ctx: any) {
scheduleStreamingFlush();
}
ctx.monitorEndModelOutput();
+ flushPendingToolEvents();
});
// 工具提示事件(可选)
@@ -863,46 +933,76 @@ export async function initializeLegacySocket(ctx: any) {
return;
}
socketLog('工具准备中:', data.name);
+ if (typeof console !== 'undefined' && console.debug) {
+ console.debug('[tool_intent] preparing', {
+ id: data?.id,
+ name: data?.name,
+ intent: data?.intent,
+ convo: data?.conversation_id
+ });
+ }
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
socketLog('跳过tool_preparing(对话不匹配)', data.conversation_id);
return;
}
- const handler = () => {
- const msg = ctx.chatEnsureAssistantMessage();
- if (!msg) {
- return;
- }
- const action = {
- id: data.id,
- type: 'tool',
- tool: {
- id: data.id,
- name: data.name,
- arguments: {},
- argumentSnapshot: null,
- argumentLabel: '',
- status: 'preparing',
- result: null,
- message: data.message || `准备调用 ${data.name}...`
- },
- timestamp: Date.now()
- };
- msg.actions.push(action);
- ctx.preparingTools.set(data.id, action);
- ctx.toolRegisterAction(action, data.id);
- ctx.toolTrackAction(data.name, action);
- ctx.$forceUpdate();
- ctx.conditionalScrollToBottom();
- if (ctx.monitorPreviewTool) {
- ctx.monitorPreviewTool(data);
- }
- };
-
- if (hasPendingStreamingText()) {
- pendingToolEvents.push({ event: 'tool_preparing', data, handler });
- } else {
- handler();
+ const msg = ctx.chatEnsureAssistantMessage();
+ if (!msg) {
+ return;
}
+ const action = {
+ id: data.id,
+ type: 'tool',
+ tool: {
+ id: data.id,
+ name: data.name,
+ arguments: {},
+ argumentSnapshot: null,
+ argumentLabel: '',
+ status: 'preparing',
+ result: null,
+ message: data.message || `准备调用 ${data.name}...`,
+ intent_full: data.intent || '',
+ intent_rendered: data.intent || ''
+ },
+ timestamp: Date.now()
+ };
+ msg.actions.push(action);
+ ctx.preparingTools.set(data.id, action);
+ ctx.toolRegisterAction(action, data.id);
+ ctx.toolTrackAction(data.name, action);
+ if (data.intent) {
+ startIntentTyping(action, data.intent);
+ }
+ ctx.$forceUpdate();
+ ctx.conditionalScrollToBottom();
+ if (ctx.monitorPreviewTool) {
+ ctx.monitorPreviewTool(data);
+ }
+ });
+
+ // 工具意图(流式增量)事件
+ ctx.socket.on('tool_intent', (data) => {
+ if (ctx.dropToolEvents) {
+ return;
+ }
+ if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
+ return;
+ }
+ if (typeof console !== 'undefined' && console.debug) {
+ console.debug('[tool_intent] update', {
+ id: data?.id,
+ name: data?.name,
+ intent: data?.intent,
+ convo: data?.conversation_id
+ });
+ }
+ const target =
+ ctx.toolFindAction(data.id, data.preparing_id, data.execution_id) ||
+ ctx.toolGetLatestAction(data.name);
+ if (target) {
+ startIntentTyping(target, data.intent);
+ }
+ ctx.$forceUpdate();
});
// 工具状态更新事件 - 实时显示详细状态
@@ -919,6 +1019,9 @@ export async function initializeLegacySocket(ctx: any) {
if (target) {
target.tool.statusDetail = data.detail;
target.tool.statusType = data.status;
+ if (data.intent) {
+ startIntentTyping(target, data.intent);
+ }
ctx.$forceUpdate();
return;
}
@@ -927,6 +1030,9 @@ export async function initializeLegacySocket(ctx: any) {
fallbackAction.tool.statusDetail = data.detail;
fallbackAction.tool.statusType = data.status;
ctx.toolRegisterAction(fallbackAction, data.execution_id || data.id || data.preparing_id);
+ if (data.intent) {
+ startIntentTyping(fallbackAction, data.intent);
+ }
ctx.$forceUpdate();
}
});
@@ -977,6 +1083,9 @@ export async function initializeLegacySocket(ctx: any) {
action.tool.message = null;
action.tool.id = data.id;
action.tool.executionId = data.id;
+ if (data.arguments && data.arguments.intent) {
+ startIntentTyping(action, data.arguments.intent);
+ }
ctx.toolRegisterAction(action, data.id);
ctx.toolTrackAction(data.name, action);
ctx.$forceUpdate();
@@ -984,11 +1093,7 @@ export async function initializeLegacySocket(ctx: any) {
ctx.monitorQueueTool(data);
};
- if (hasPendingStreamingText()) {
- pendingToolEvents.push({ event: 'tool_start', data, handler });
- } else {
- handler();
- }
+ handler();
});
// 更新action(工具完成)
@@ -1039,6 +1144,9 @@ export async function initializeLegacySocket(ctx: any) {
if (data.message !== undefined) {
targetAction.tool.message = data.message;
}
+ if (data.arguments && data.arguments.intent) {
+ startIntentTyping(targetAction, data.arguments.intent);
+ }
if (data.awaiting_content) {
targetAction.tool.awaiting_content = true;
} else if (data.status === 'completed') {
@@ -1051,6 +1159,9 @@ export async function initializeLegacySocket(ctx: any) {
targetAction.tool.arguments = data.arguments;
targetAction.tool.argumentSnapshot = ctx.cloneToolArguments(data.arguments);
targetAction.tool.argumentLabel = ctx.buildToolLabel(targetAction.tool.argumentSnapshot);
+ if (data.arguments.intent) {
+ startIntentTyping(targetAction, data.arguments.intent);
+ }
}
ctx.toolRegisterAction(targetAction, data.execution_id || data.id);
if (data.status && ["completed", "failed", "error"].includes(data.status) && !data.awaiting_content) {
@@ -1079,11 +1190,7 @@ export async function initializeLegacySocket(ctx: any) {
ctx.monitorResolveTool(data);
};
- if (hasPendingStreamingText()) {
- pendingToolEvents.push({ event: 'update_action', data, handler });
- } else {
- handler();
- }
+ handler();
});
ctx.socket.on('append_payload', (data) => {
diff --git a/static/src/composables/useScrollControl.ts b/static/src/composables/useScrollControl.ts
index 9d64d16..fb95fa4 100644
--- a/static/src/composables/useScrollControl.ts
+++ b/static/src/composables/useScrollControl.ts
@@ -11,21 +11,38 @@ type ScrollContext = {
};
export function scrollToBottom(ctx: ScrollContext) {
- setTimeout(() => {
- const messagesArea = ctx.getMessagesAreaElement?.();
- if (!messagesArea) {
- return;
- }
- if (typeof ctx._setScrollingFlag === 'function') {
- ctx._setScrollingFlag(true);
- }
- messagesArea.scrollTop = messagesArea.scrollHeight;
- setTimeout(() => {
+ const messagesArea = ctx.getMessagesAreaElement?.();
+ if (!messagesArea) {
+ return;
+ }
+
+ const attempts = [0, 16, 60, 150, 320, 520]; // 多次尝试覆盖布局抖动/异步伸缩
+ let cancelled = false;
+
+ const perform = (idx: number) => {
+ if (cancelled) return;
+ if (ctx.userScrolling) {
+ cancelled = true;
if (typeof ctx._setScrollingFlag === 'function') {
ctx._setScrollingFlag(false);
}
- }, 100);
- }, 50);
+ return;
+ }
+
+ if (idx === 0 && typeof ctx._setScrollingFlag === 'function') {
+ ctx._setScrollingFlag(true);
+ }
+
+ messagesArea.scrollTop = messagesArea.scrollHeight;
+
+ if (idx < attempts.length - 1) {
+ setTimeout(() => perform(idx + 1), attempts[idx + 1] - attempts[idx]);
+ } else if (typeof ctx._setScrollingFlag === 'function') {
+ setTimeout(() => ctx._setScrollingFlag && ctx._setScrollingFlag(false), 40);
+ }
+ };
+
+ perform(0);
}
export function conditionalScrollToBottom(ctx: ScrollContext) {
diff --git a/static/src/stores/personalization.ts b/static/src/stores/personalization.ts
index bed9d9c..49beb40 100644
--- a/static/src/stores/personalization.ts
+++ b/static/src/stores/personalization.ts
@@ -5,6 +5,7 @@ type RunMode = 'fast' | 'thinking' | 'deep';
interface PersonalForm {
enabled: boolean;
auto_generate_title: boolean;
+ tool_intent_enabled: boolean;
self_identify: string;
user_name: string;
profession: string;
@@ -54,6 +55,7 @@ const EXPERIMENT_STORAGE_KEY = 'agents_personalization_experiments';
const defaultForm = (): PersonalForm => ({
enabled: false,
auto_generate_title: true,
+ tool_intent_enabled: true,
self_identify: '',
user_name: '',
profession: '',
@@ -176,6 +178,7 @@ export const usePersonalizationStore = defineStore('personalization', {
this.form = {
enabled: !!data.enabled,
auto_generate_title: data.auto_generate_title !== false,
+ tool_intent_enabled: !!data.tool_intent_enabled,
self_identify: data.self_identify || '',
user_name: data.user_name || '',
profession: data.profession || '',
diff --git a/static/src/styles/components/chat/_chat-area.scss b/static/src/styles/components/chat/_chat-area.scss
index 396c768..aa35cb9 100644
--- a/static/src/styles/components/chat/_chat-area.scss
+++ b/static/src/styles/components/chat/_chat-area.scss
@@ -406,8 +406,10 @@
transform: translateY(14px);
}
-.stacked-enter-active {
+.stacked-enter-active,
+.stacked-appear-active {
transition: all 220ms cubic-bezier(0.4, 0, 0.2, 1);
+ transition-delay: 120ms;
}
.stacked-leave-active {
@@ -421,6 +423,11 @@
transform: translateY(-12px);
}
+.stacked-appear-from {
+ opacity: 0;
+ transform: translateY(14px);
+}
+
.stacked-move {
transition: transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
}
@@ -440,6 +447,7 @@
animation: progress-bar 2s ease-in-out infinite;
opacity: 0;
transition: opacity 0.3s ease;
+ pointer-events: none;
}
.collapsible-block.processing .progress-indicator {
diff --git a/static/src/utils/chatDisplay.ts b/static/src/utils/chatDisplay.ts
index dc6cee9..2c0e4ca 100644
--- a/static/src/utils/chatDisplay.ts
+++ b/static/src/utils/chatDisplay.ts
@@ -148,10 +148,28 @@ function describeReadFileResult(tool: any): string {
return '文件读取完成';
}
-export function getToolStatusText(tool: any): string {
+export function getToolStatusText(tool: any, opts?: { intentEnabled?: boolean }): string {
if (!tool) {
return '';
}
+ const intentEnabled = opts?.intentEnabled !== false;
+ const intentText = tool.intent_rendered || tool.intent_full || '';
+ const hasIntent = !!intentText;
+
+ // 错误优先展示
+ if (
+ tool.message &&
+ (tool.status === 'failed' || tool.status === 'error' || (tool.result && tool.result.error))
+ ) {
+ return tool.message;
+ }
+
+ // 开启时:有 intent 就只显示 intent
+ if (intentEnabled && hasIntent) {
+ return intentText;
+ }
+
+ // 关闭 intent 或无 intent 时,显示状态文案
if (tool.message) {
return tool.message;
}
@@ -186,7 +204,10 @@ export function getToolStatusText(tool: any): string {
}
return COMPLETED_STATUS_TEXTS[tool.name] || '执行完成';
}
- return `${tool.name} - ${tool.status}`;
+ if (tool.status) {
+ return `${tool.name} - ${tool.status}`;
+ }
+ return tool.name || '';
}
export function getToolDescription(tool: any): string {
diff --git a/web_server.py b/web_server.py
index 83d0656..e602da5 100644
--- a/web_server.py
+++ b/web_server.py
@@ -2984,6 +2984,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
last_tool_name = ""
auto_fix_attempts = 0
last_tool_call_time = 0
+ detected_tool_intent: Dict[str, str] = {}
# 设置最大迭代次数
max_iterations = MAX_ITERATIONS_PER_TASK
@@ -2993,6 +2994,17 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
pending_modify = None # {"path": str, "tool_call_id": str, "buffer": str, ...}
modify_probe_buffer = ""
+ def extract_intent_from_partial(arg_str: str) -> Optional[str]:
+ """从不完整的JSON字符串中粗略提取 intent 字段,容错用于流式阶段。"""
+ if not arg_str or "intent" not in arg_str:
+ return None
+ import re
+ # 匹配 "intent": "xxx" 形式,允许前面有换行或空格;宽松匹配未闭合的引号
+ match = re.search(r'"intent"\s*:\s*"([^"]{0,128})', arg_str, re.IGNORECASE | re.DOTALL)
+ if match:
+ return match.group(1)
+ return None
+
def resolve_monitor_path(args: Dict[str, Any], fallback: Optional[str] = None) -> Optional[str]:
candidates = [
args.get('path'),
@@ -3987,24 +3999,62 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
for existing in tool_calls:
if existing.get("index") == tc.get("index"):
if "function" in tc and "arguments" in tc["function"]:
- existing["function"]["arguments"] += tc["function"]["arguments"]
+ arg_chunk = tc["function"]["arguments"]
+ existing_fn = existing.get("function", {})
+ existing_args = existing_fn.get("arguments", "")
+ existing_fn["arguments"] = (existing_args or "") + arg_chunk
+ existing["function"] = existing_fn
+
+ combined_args = existing_fn.get("arguments", "")
+ tool_id = existing.get("id") or tc.get("id")
+ tool_name = (
+ existing_fn.get("name")
+ or tc.get("function", {}).get("name", "")
+ )
+ intent_value = extract_intent_from_partial(combined_args)
+ if (
+ intent_value
+ and tool_id
+ and detected_tool_intent.get(tool_id) != intent_value
+ ):
+ detected_tool_intent[tool_id] = intent_value
+ brief_log(f"[intent] 增量提取 {tool_name}: {intent_value}")
+ sender('tool_intent', {
+ 'id': tool_id,
+ 'name': tool_name,
+ 'intent': intent_value,
+ 'conversation_id': conversation_id
+ })
+ debug_log(f" 发送工具意图: {tool_name} -> {intent_value}")
+ await asyncio.sleep(0.01)
found = True
break
if not found and tc.get("id"):
tool_id = tc["id"]
tool_name = tc.get("function", {}).get("name", "")
+ arguments_str = tc.get("function", {}).get("arguments", "") or ""
# 新工具检测到,立即发送准备事件
if tool_id not in detected_tools and tool_name:
detected_tools[tool_id] = tool_name
+ # 尝试提前提取 intent
+ intent_value = None
+ if arguments_str:
+ intent_value = extract_intent_from_partial(arguments_str)
+ if intent_value:
+ detected_tool_intent[tool_id] = intent_value
+ brief_log(f"[intent] 预提取 {tool_name}: {intent_value}")
+
# 立即发送工具准备中事件
+ brief_log(f"[tool] 准备调用 {tool_name} (id={tool_id}) intent={intent_value or '-'}")
sender('tool_preparing', {
'id': tool_id,
'name': tool_name,
- 'message': f'准备调用 {tool_name}...'
- , 'conversation_id': conversation_id
+ 'message': f'准备调用 {tool_name}...',
+ 'intent': intent_value,
+ 'conversation_id': conversation_id
})
debug_log(f" 发送工具准备事件: {tool_name}")
await asyncio.sleep(0.1)
@@ -4015,9 +4065,22 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
"type": "function",
"function": {
"name": tool_name,
- "arguments": tc.get("function", {}).get("arguments", "")
+ "arguments": arguments_str
}
})
+ # 尝试从增量参数中抽取 intent,并单独推送
+ if tool_id and arguments_str:
+ intent_value = extract_intent_from_partial(arguments_str)
+ if intent_value and detected_tool_intent.get(tool_id) != intent_value:
+ detected_tool_intent[tool_id] = intent_value
+ sender('tool_intent', {
+ 'id': tool_id,
+ 'name': tool_name,
+ 'intent': intent_value,
+ 'conversation_id': conversation_id
+ })
+ debug_log(f" 发送工具意图: {tool_name} -> {intent_value}")
+ await asyncio.sleep(0.01)
debug_log(f" 新工具: {tool_name}")
# 检查是否被停止