fix(ui): refine message visibility and goal approval flow
This commit is contained in:
parent
e220b3703a
commit
32865f6d6e
@ -141,6 +141,37 @@ from .goal_flow import (
|
|||||||
emit_goal_progress,
|
emit_goal_progress,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_VALID_USER_MESSAGE_SOURCES = {
|
||||||
|
"user",
|
||||||
|
"guidance",
|
||||||
|
"notify",
|
||||||
|
"presend",
|
||||||
|
"sub_agent",
|
||||||
|
"background_command",
|
||||||
|
"goal",
|
||||||
|
"goal_prompt",
|
||||||
|
"goal_review",
|
||||||
|
"compression",
|
||||||
|
"permission",
|
||||||
|
"sandbox",
|
||||||
|
"skill",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _user_message_ui_defaults(source: str, *, auto_user_message_event: bool = False) -> Dict[str, Any]:
|
||||||
|
normalized = str(source or "").strip().lower()
|
||||||
|
if normalized in {"skill", "goal_prompt", "permission", "sandbox"}:
|
||||||
|
return {"visibility": "hidden", "starts_work": False}
|
||||||
|
if normalized in {"guidance", "notify"}:
|
||||||
|
return {"visibility": "compact", "starts_work": False}
|
||||||
|
if normalized in {"goal_review", "compression", "sub_agent", "background_command"}:
|
||||||
|
return {"visibility": "compact", "starts_work": True}
|
||||||
|
if normalized == "presend":
|
||||||
|
return {"visibility": "chat", "starts_work": True}
|
||||||
|
if normalized == "goal":
|
||||||
|
return {"visibility": "compact", "starts_work": True}
|
||||||
|
return {"visibility": "chat", "starts_work": normalized == "user" and not auto_user_message_event}
|
||||||
|
|
||||||
|
|
||||||
def _should_skip_versioning_for_message(
|
def _should_skip_versioning_for_message(
|
||||||
*,
|
*,
|
||||||
@ -308,7 +339,7 @@ async def _dispatch_completion_user_notice(
|
|||||||
message_source = "background_command"
|
message_source = "background_command"
|
||||||
elif extra_payload.get("sub_agent_notice"):
|
elif extra_payload.get("sub_agent_notice"):
|
||||||
message_source = "sub_agent"
|
message_source = "sub_agent"
|
||||||
if message_source not in {"user", "guidance", "notify", "presend", "sub_agent", "background_command", "goal"}:
|
if message_source not in _VALID_USER_MESSAGE_SOURCES:
|
||||||
message_source = "user"
|
message_source = "user"
|
||||||
try:
|
try:
|
||||||
from .tasks import task_manager
|
from .tasks import task_manager
|
||||||
@ -326,10 +357,17 @@ async def _dispatch_completion_user_notice(
|
|||||||
"model_key": getattr(web_terminal, "model_key", None),
|
"model_key": getattr(web_terminal, "model_key", None),
|
||||||
"message_source": message_source,
|
"message_source": message_source,
|
||||||
}
|
}
|
||||||
|
ui_defaults = _user_message_ui_defaults(
|
||||||
|
message_source,
|
||||||
|
auto_user_message_event=True,
|
||||||
|
)
|
||||||
# 关键:通知类后台任务需要把 user_message 写入任务事件流,
|
# 关键:通知类后台任务需要把 user_message 写入任务事件流,
|
||||||
# 否则前端轮询只会看到 AI/tool 事件,看不到 user_message。
|
# 否则前端轮询只会看到 AI/tool 事件,看不到 user_message。
|
||||||
session_data["auto_user_message_event"] = True
|
session_data["auto_user_message_event"] = True
|
||||||
session_data["auto_user_message_payload"] = dict(extra_payload or {})
|
session_data["auto_user_message_payload"] = {
|
||||||
|
**dict(extra_payload or {}),
|
||||||
|
**ui_defaults,
|
||||||
|
}
|
||||||
rec = task_manager.create_chat_task(
|
rec = task_manager.create_chat_task(
|
||||||
username,
|
username,
|
||||||
workspace_id,
|
workspace_id,
|
||||||
@ -346,8 +384,14 @@ async def _dispatch_completion_user_notice(
|
|||||||
'conversation_id': conversation_id,
|
'conversation_id': conversation_id,
|
||||||
'task_id': rec.task_id,
|
'task_id': rec.task_id,
|
||||||
'message_source': message_source,
|
'message_source': message_source,
|
||||||
|
**ui_defaults,
|
||||||
}
|
}
|
||||||
payload.update(extra_payload)
|
payload.update(extra_payload)
|
||||||
|
payload["metadata"] = {
|
||||||
|
"message_source": message_source,
|
||||||
|
**ui_defaults,
|
||||||
|
**(payload.get("metadata") or {}),
|
||||||
|
}
|
||||||
sender('user_message', payload)
|
sender('user_message', payload)
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -358,7 +402,14 @@ async def _dispatch_completion_user_notice(
|
|||||||
'conversation_id': conversation_id,
|
'conversation_id': conversation_id,
|
||||||
'message_source': message_source,
|
'message_source': message_source,
|
||||||
}
|
}
|
||||||
|
payload.update(_user_message_ui_defaults(message_source, auto_user_message_event=True))
|
||||||
payload.update(extra_payload)
|
payload.update(extra_payload)
|
||||||
|
payload["metadata"] = {
|
||||||
|
"message_source": message_source,
|
||||||
|
"visibility": payload.get("visibility"),
|
||||||
|
"starts_work": payload.get("starts_work"),
|
||||||
|
**(payload.get("metadata") or {}),
|
||||||
|
}
|
||||||
sender('user_message', payload)
|
sender('user_message', payload)
|
||||||
try:
|
try:
|
||||||
task_handle = asyncio.create_task(handle_task_with_sender(
|
task_handle = asyncio.create_task(handle_task_with_sender(
|
||||||
@ -789,7 +840,8 @@ async def handle_task_with_sender(
|
|||||||
# 添加到对话历史
|
# 添加到对话历史
|
||||||
user_work_started_at = datetime.now().isoformat()
|
user_work_started_at = datetime.now().isoformat()
|
||||||
user_message_index = -1
|
user_message_index = -1
|
||||||
user_work_finalized = False
|
current_work_user_message_index = -1
|
||||||
|
finalized_work_message_indices: set[int] = set()
|
||||||
history_len_before = len(getattr(web_terminal.context_manager, "conversation_history", []) or [])
|
history_len_before = len(getattr(web_terminal.context_manager, "conversation_history", []) or [])
|
||||||
is_first_user_message = history_len_before == 0
|
is_first_user_message = history_len_before == 0
|
||||||
# 构建 user 消息来源与 metadata
|
# 构建 user 消息来源与 metadata
|
||||||
@ -809,14 +861,20 @@ async def handle_task_with_sender(
|
|||||||
user_message_source = "background_command"
|
user_message_source = "background_command"
|
||||||
elif auto_payload.get("sub_agent_notice"):
|
elif auto_payload.get("sub_agent_notice"):
|
||||||
user_message_source = "sub_agent"
|
user_message_source = "sub_agent"
|
||||||
if user_message_source not in {"user", "guidance", "notify", "presend", "sub_agent", "background_command", "goal"}:
|
if user_message_source not in _VALID_USER_MESSAGE_SOURCES:
|
||||||
user_message_source = "user"
|
user_message_source = "user"
|
||||||
|
|
||||||
# 构建user消息metadata
|
# 构建user消息metadata
|
||||||
user_message_metadata = {
|
user_message_metadata = {
|
||||||
"message_source": user_message_source,
|
"message_source": user_message_source,
|
||||||
}
|
}
|
||||||
if user_message_source in {"user", "presend", "sub_agent", "background_command"}:
|
user_message_metadata.update(
|
||||||
|
_user_message_ui_defaults(
|
||||||
|
user_message_source,
|
||||||
|
auto_user_message_event=bool(auto_user_message_event),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if user_message_metadata.get("starts_work") is True:
|
||||||
user_message_metadata["work_timer"] = {
|
user_message_metadata["work_timer"] = {
|
||||||
"status": "working",
|
"status": "working",
|
||||||
"started_at": user_work_started_at
|
"started_at": user_work_started_at
|
||||||
@ -836,6 +894,7 @@ async def handle_task_with_sender(
|
|||||||
user_message_index = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) - 1
|
user_message_index = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) - 1
|
||||||
except Exception:
|
except Exception:
|
||||||
user_message_index = -1
|
user_message_index = -1
|
||||||
|
current_work_user_message_index = user_message_index
|
||||||
skill_context_messages = getattr(web_terminal, "_skill_context_messages", None)
|
skill_context_messages = getattr(web_terminal, "_skill_context_messages", None)
|
||||||
if not auto_user_message_event and isinstance(skill_context_messages, list):
|
if not auto_user_message_event and isinstance(skill_context_messages, list):
|
||||||
for skill_item in skill_context_messages:
|
for skill_item in skill_context_messages:
|
||||||
@ -852,6 +911,8 @@ async def handle_task_with_sender(
|
|||||||
metadata={
|
metadata={
|
||||||
"source": "skill",
|
"source": "skill",
|
||||||
"message_source": "skill",
|
"message_source": "skill",
|
||||||
|
"visibility": "hidden",
|
||||||
|
"starts_work": False,
|
||||||
"hidden": True,
|
"hidden": True,
|
||||||
"skill_name": skill_name,
|
"skill_name": skill_name,
|
||||||
"skill_path": skill_path,
|
"skill_path": skill_path,
|
||||||
@ -867,6 +928,9 @@ async def handle_task_with_sender(
|
|||||||
"videos": (saved_user_message or {}).get("videos") or videos or [],
|
"videos": (saved_user_message or {}).get("videos") or videos or [],
|
||||||
"media_refs": (saved_user_message or {}).get("media_refs") or [],
|
"media_refs": (saved_user_message or {}).get("media_refs") or [],
|
||||||
"message_source": user_message_source,
|
"message_source": user_message_source,
|
||||||
|
"visibility": user_message_metadata.get("visibility"),
|
||||||
|
"starts_work": user_message_metadata.get("starts_work"),
|
||||||
|
"metadata": user_message_metadata,
|
||||||
"conversation_id": conversation_id,
|
"conversation_id": conversation_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -880,14 +944,14 @@ async def handle_task_with_sender(
|
|||||||
auto_user_message_event=bool(auto_user_message_event),
|
auto_user_message_event=bool(auto_user_message_event),
|
||||||
)
|
)
|
||||||
|
|
||||||
def finalize_user_work_timer():
|
def finalize_user_work_timer(index: Optional[int] = None):
|
||||||
nonlocal user_work_finalized
|
target_index = current_work_user_message_index if index is None else index
|
||||||
if user_work_finalized:
|
if target_index in finalized_work_message_indices:
|
||||||
return
|
return
|
||||||
history = getattr(web_terminal.context_manager, "conversation_history", None) or []
|
history = getattr(web_terminal.context_manager, "conversation_history", None) or []
|
||||||
if user_message_index < 0 or user_message_index >= len(history):
|
if target_index < 0 or target_index >= len(history):
|
||||||
return
|
return
|
||||||
target_msg = history[user_message_index] or {}
|
target_msg = history[target_index] or {}
|
||||||
if target_msg.get("role") != "user":
|
if target_msg.get("role") != "user":
|
||||||
return
|
return
|
||||||
metadata = target_msg.get("metadata") or {}
|
metadata = target_msg.get("metadata") or {}
|
||||||
@ -910,9 +974,20 @@ async def handle_task_with_sender(
|
|||||||
})
|
})
|
||||||
metadata["work_timer"] = timer
|
metadata["work_timer"] = timer
|
||||||
target_msg["metadata"] = metadata
|
target_msg["metadata"] = metadata
|
||||||
history[user_message_index] = target_msg
|
history[target_index] = target_msg
|
||||||
web_terminal.context_manager.auto_save_conversation(force=True)
|
web_terminal.context_manager.auto_save_conversation(force=True)
|
||||||
user_work_finalized = True
|
finalized_work_message_indices.add(target_index)
|
||||||
|
|
||||||
|
def switch_current_work_timer_to_latest_user():
|
||||||
|
nonlocal current_work_user_message_index
|
||||||
|
history = getattr(web_terminal.context_manager, "conversation_history", None) or []
|
||||||
|
for idx in range(len(history) - 1, -1, -1):
|
||||||
|
msg = history[idx] or {}
|
||||||
|
if msg.get("role") == "user":
|
||||||
|
metadata = msg.get("metadata") or {}
|
||||||
|
if isinstance(metadata.get("work_timer"), dict):
|
||||||
|
current_work_user_message_index = idx
|
||||||
|
return
|
||||||
|
|
||||||
versioning_checkpoint_recorded = False
|
versioning_checkpoint_recorded = False
|
||||||
|
|
||||||
@ -1394,7 +1469,10 @@ async def handle_task_with_sender(
|
|||||||
)
|
)
|
||||||
action = goal_result.get("action")
|
action = goal_result.get("action")
|
||||||
if action == "continue":
|
if action == "continue":
|
||||||
# 已注入续命 user 消息,重置目标轮段标志并继续主循环
|
# 已注入续命 user 消息:上一段 assistant work 到此结束,
|
||||||
|
# 后续计时切换到刚注入的 goal_review 消息。
|
||||||
|
finalize_user_work_timer()
|
||||||
|
switch_current_work_timer_to_latest_user()
|
||||||
goal_segment_made_tool_call = False
|
goal_segment_made_tool_call = False
|
||||||
is_first_iteration = False
|
is_first_iteration = False
|
||||||
debug_log(f"[Goal] 续命:{goal_result.get('message', '')[:80]}")
|
debug_log(f"[Goal] 续命:{goal_result.get('message', '')[:80]}")
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
|
from datetime import datetime
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
from modules.sub_agent_manager import TERMINAL_STATUSES
|
from modules.sub_agent_manager import TERMINAL_STATUSES
|
||||||
@ -15,9 +16,31 @@ _VALID_SOURCES = {
|
|||||||
"sub_agent",
|
"sub_agent",
|
||||||
"background_command",
|
"background_command",
|
||||||
"goal",
|
"goal",
|
||||||
|
"goal_prompt",
|
||||||
|
"goal_review",
|
||||||
|
"compression",
|
||||||
|
"permission",
|
||||||
|
"sandbox",
|
||||||
|
"skill",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_message_ui_defaults(src: str, *, inline: bool = False) -> Dict[str, Any]:
|
||||||
|
"""Return UI metadata for model-facing user messages injected by the runtime."""
|
||||||
|
normalized = str(src or "").strip().lower()
|
||||||
|
if normalized in {"skill", "goal_prompt", "permission", "sandbox"}:
|
||||||
|
return {"visibility": "hidden", "starts_work": False}
|
||||||
|
if normalized in {"guidance", "notify"}:
|
||||||
|
return {"visibility": "compact", "starts_work": False}
|
||||||
|
if normalized in {"goal_review", "compression", "sub_agent", "background_command", "presend"}:
|
||||||
|
return {"visibility": "compact", "starts_work": True}
|
||||||
|
if normalized == "goal":
|
||||||
|
# Backward-compatible fallback for old callers. Inline goal injections are
|
||||||
|
# current-segment hints; non-inline goal injections start the next segment.
|
||||||
|
return {"visibility": "compact", "starts_work": not bool(inline)}
|
||||||
|
return {"visibility": "chat", "starts_work": normalized == "user"}
|
||||||
|
|
||||||
|
|
||||||
def inject_runtime_user_message(
|
def inject_runtime_user_message(
|
||||||
*,
|
*,
|
||||||
web_terminal,
|
web_terminal,
|
||||||
@ -57,8 +80,14 @@ def inject_runtime_user_message(
|
|||||||
"runtime_guidance": True,
|
"runtime_guidance": True,
|
||||||
"runtime_guidance_original": raw,
|
"runtime_guidance_original": raw,
|
||||||
}
|
}
|
||||||
|
metadata.update(_runtime_message_ui_defaults(src, inline=inline))
|
||||||
if extra_metadata:
|
if extra_metadata:
|
||||||
metadata.update({k: v for k, v in extra_metadata.items() if v is not None})
|
metadata.update({k: v for k, v in extra_metadata.items() if v is not None})
|
||||||
|
if metadata.get("starts_work") is True and not isinstance(metadata.get("work_timer"), dict):
|
||||||
|
metadata["work_timer"] = {
|
||||||
|
"status": "working",
|
||||||
|
"started_at": datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
if persist:
|
if persist:
|
||||||
try:
|
try:
|
||||||
@ -93,6 +122,9 @@ def inject_runtime_user_message(
|
|||||||
"inline": inline,
|
"inline": inline,
|
||||||
"source": src,
|
"source": src,
|
||||||
"message_source": src,
|
"message_source": src,
|
||||||
|
"visibility": metadata.get("visibility"),
|
||||||
|
"starts_work": metadata.get("starts_work"),
|
||||||
|
"metadata": metadata,
|
||||||
"runtime_guidance": True,
|
"runtime_guidance": True,
|
||||||
"runtime_guidance_original": raw,
|
"runtime_guidance_original": raw,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,7 +97,7 @@ def inject_goal_prompt(
|
|||||||
web_terminal=web_terminal,
|
web_terminal=web_terminal,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
text=GOAL_MODE_PROMPT,
|
text=GOAL_MODE_PROMPT,
|
||||||
source="goal",
|
source="goal_prompt",
|
||||||
sender=sender,
|
sender=sender,
|
||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
inline=False,
|
inline=False,
|
||||||
@ -231,7 +231,7 @@ async def handle_goal_after_turn(
|
|||||||
web_terminal=web_terminal,
|
web_terminal=web_terminal,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
text=f"{CONTINUE_PREFIX}{message}",
|
text=f"{CONTINUE_PREFIX}{message}",
|
||||||
source="goal",
|
source="goal_review",
|
||||||
sender=sender,
|
sender=sender,
|
||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
inline=False,
|
inline=False,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { debugLog } from './common';
|
import { debugLog } from './common';
|
||||||
import { useTaskStore } from '../../stores/task';
|
import { useTaskStore } from '../../stores/task';
|
||||||
|
import { getMessageVisibility, messageStartsWork } from '../../utils/messageVisibility';
|
||||||
|
|
||||||
const debugNotifyLog = (...args: any[]) => {
|
const debugNotifyLog = (...args: any[]) => {
|
||||||
void args;
|
void args;
|
||||||
@ -114,6 +115,23 @@ function resolveUserMessageSource(data: any): string {
|
|||||||
return 'user';
|
return 'user';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveUserMessageMetadata(data: any, source: string, message: string): Record<string, any> {
|
||||||
|
const base = data && typeof data.metadata === 'object' && data.metadata ? { ...data.metadata } : {};
|
||||||
|
const metadata: Record<string, any> = {
|
||||||
|
...base,
|
||||||
|
message_source: source
|
||||||
|
};
|
||||||
|
if (data && Object.prototype.hasOwnProperty.call(data, 'visibility')) {
|
||||||
|
metadata.visibility = data.visibility;
|
||||||
|
}
|
||||||
|
if (data && Object.prototype.hasOwnProperty.call(data, 'starts_work')) {
|
||||||
|
metadata.starts_work = data.starts_work;
|
||||||
|
}
|
||||||
|
metadata.visibility = getMessageVisibility({ role: 'user', content: message, metadata, ...data });
|
||||||
|
metadata.starts_work = messageStartsWork({ role: 'user', content: message, metadata, ...data });
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
function isEmptyAssistantPlaceholderMessage(message: any): boolean {
|
function isEmptyAssistantPlaceholderMessage(message: any): boolean {
|
||||||
if (!message || message.role !== 'assistant') {
|
if (!message || message.role !== 'assistant') {
|
||||||
return false;
|
return false;
|
||||||
@ -1912,10 +1930,6 @@ export const taskPollingMethods = {
|
|||||||
this.autoApprovalFeedLines.push(String(progress.message));
|
this.autoApprovalFeedLines.push(String(progress.message));
|
||||||
}
|
}
|
||||||
this.autoApprovalFeedLines = this.autoApprovalFeedLines.slice(-20);
|
this.autoApprovalFeedLines = this.autoApprovalFeedLines.slice(-20);
|
||||||
this.rightCollapsed = false;
|
|
||||||
if (this.rightWidth < this.minPanelWidth) {
|
|
||||||
this.rightWidth = this.minPanelWidth;
|
|
||||||
}
|
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -2044,6 +2058,7 @@ export const taskPollingMethods = {
|
|||||||
const isAutoUserMessage = isSystemAutoUserMessagePayload(data);
|
const isAutoUserMessage = isSystemAutoUserMessagePayload(data);
|
||||||
const isRuntimeModeNotice = isRuntimeModeNoticePayload(data);
|
const isRuntimeModeNotice = isRuntimeModeNoticePayload(data);
|
||||||
const source = resolveUserMessageSource(data);
|
const source = resolveUserMessageSource(data);
|
||||||
|
const eventMetadata = resolveUserMessageMetadata(data, source, message);
|
||||||
const incomingImages = Array.isArray(data?.images) ? data.images : [];
|
const incomingImages = Array.isArray(data?.images) ? data.images : [];
|
||||||
const incomingVideos = Array.isArray(data?.videos) ? data.videos : [];
|
const incomingVideos = Array.isArray(data?.videos) ? data.videos : [];
|
||||||
const incomingMediaRefs = Array.isArray(data?.media_refs)
|
const incomingMediaRefs = Array.isArray(data?.media_refs)
|
||||||
@ -2072,6 +2087,7 @@ export const taskPollingMethods = {
|
|||||||
target.media_refs = incomingMediaRefs;
|
target.media_refs = incomingMediaRefs;
|
||||||
target.metadata = {
|
target.metadata = {
|
||||||
...(target.metadata || {}),
|
...(target.metadata || {}),
|
||||||
|
...eventMetadata,
|
||||||
media_refs: incomingMediaRefs
|
media_refs: incomingMediaRefs
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@ -2080,7 +2096,8 @@ export const taskPollingMethods = {
|
|||||||
incomingImages,
|
incomingImages,
|
||||||
incomingVideos,
|
incomingVideos,
|
||||||
incomingMediaRefs,
|
incomingMediaRefs,
|
||||||
source
|
source,
|
||||||
|
eventMetadata
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.taskInProgress = true;
|
this.taskInProgress = true;
|
||||||
@ -2099,6 +2116,9 @@ export const taskPollingMethods = {
|
|||||||
messagePreview: message.slice(0, 80)
|
messagePreview: message.slice(0, 80)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
if (eventMetadata.starts_work === true) {
|
||||||
|
this.markLatestUserWorkCompleted();
|
||||||
|
}
|
||||||
const shouldRestoreWaitingPlaceholder =
|
const shouldRestoreWaitingPlaceholder =
|
||||||
this.moveTrailingEmptyAssistantPlaceholderAfterUserInsert('auto_user_message') &&
|
this.moveTrailingEmptyAssistantPlaceholderAfterUserInsert('auto_user_message') &&
|
||||||
(this.taskInProgress || this.streamingMessage);
|
(this.taskInProgress || this.streamingMessage);
|
||||||
@ -2113,6 +2133,7 @@ export const taskPollingMethods = {
|
|||||||
recentMatch.media_refs = incomingMediaRefs;
|
recentMatch.media_refs = incomingMediaRefs;
|
||||||
recentMatch.metadata = {
|
recentMatch.metadata = {
|
||||||
...(recentMatch.metadata || {}),
|
...(recentMatch.metadata || {}),
|
||||||
|
...eventMetadata,
|
||||||
media_refs: incomingMediaRefs,
|
media_refs: incomingMediaRefs,
|
||||||
message_source: source
|
message_source: source
|
||||||
};
|
};
|
||||||
@ -2122,7 +2143,8 @@ export const taskPollingMethods = {
|
|||||||
incomingImages,
|
incomingImages,
|
||||||
incomingVideos,
|
incomingVideos,
|
||||||
incomingMediaRefs,
|
incomingMediaRefs,
|
||||||
source
|
source,
|
||||||
|
eventMetadata
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (shouldRestoreWaitingPlaceholder) {
|
if (shouldRestoreWaitingPlaceholder) {
|
||||||
|
|||||||
@ -6,11 +6,12 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="message-block"
|
class="message-block"
|
||||||
:class="{
|
:class="{
|
||||||
|
'message-block--compact-user': getMessageVisibility(msg) === 'compact',
|
||||||
'message-block--sub-agent-notice': isSubAgentNoticeOnlyMessage(msg),
|
'message-block--sub-agent-notice': isSubAgentNoticeOnlyMessage(msg),
|
||||||
'message-block--before-sub-agent-notice': isFollowedBySubAgentNotice(index)
|
'message-block--before-sub-agent-notice': isFollowedBySubAgentNotice(index)
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div v-if="msg.role === 'user'" class="user-message">
|
<div v-if="msg.role === 'user'" class="user-message" :class="{ 'user-message--compact': getMessageVisibility(msg) === 'compact' }">
|
||||||
<div class="message-header icon-label">
|
<div class="message-header icon-label">
|
||||||
<span class="icon icon-sm" :style="iconStyleSafe(userHeaderIconKey(msg))" aria-hidden="true"></span>
|
<span class="icon icon-sm" :style="iconStyleSafe(userHeaderIconKey(msg))" aria-hidden="true"></span>
|
||||||
<span>{{ userHeaderLabel(msg) }}</span>
|
<span>{{ userHeaderLabel(msg) }}</span>
|
||||||
@ -596,6 +597,7 @@ import ToolAction from '@/components/chat/actions/ToolAction.vue';
|
|||||||
import StackedBlocks from './StackedBlocks.vue';
|
import StackedBlocks from './StackedBlocks.vue';
|
||||||
import MinimalBlocks from './MinimalBlocks.vue';
|
import MinimalBlocks from './MinimalBlocks.vue';
|
||||||
import { usePersonalizationStore } from '@/stores/personalization';
|
import { usePersonalizationStore } from '@/stores/personalization';
|
||||||
|
import { getMessageVisibility, messageStartsWork } from '@/utils/messageVisibility';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
messages: Array<any>;
|
messages: Array<any>;
|
||||||
@ -677,15 +679,11 @@ function renderUserMessageContent(content: string): string {
|
|||||||
html += escapeUserHtml(source.slice(cursor));
|
html += escapeUserHtml(source.slice(cursor));
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
const isHiddenSkillMessage = (message: any) => {
|
const isHiddenUserMessage = (message: any) => {
|
||||||
if (!message || message.role !== 'user') {
|
if (!message || message.role !== 'user') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const meta = message.metadata || {};
|
return getMessageVisibility(message) === 'hidden';
|
||||||
const source = String(meta.source || meta.message_source || '')
|
|
||||||
.trim()
|
|
||||||
.toLowerCase();
|
|
||||||
return source === 'skill' || meta.hidden === true;
|
|
||||||
};
|
};
|
||||||
const isEmptyAssistantMessage = (message: any) => {
|
const isEmptyAssistantMessage = (message: any) => {
|
||||||
if (!message || message.role !== 'assistant') {
|
if (!message || message.role !== 'assistant') {
|
||||||
@ -720,7 +718,7 @@ const filteredMessages = computed(() => {
|
|||||||
if (m?.role === 'system') {
|
if (m?.role === 'system') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isHiddenSkillMessage(m)) {
|
if (isHiddenUserMessage(m)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isEmptyAssistantMessage(m)) {
|
if (isEmptyAssistantMessage(m)) {
|
||||||
@ -1353,18 +1351,15 @@ function findAssistantHeaderAnchorUser(index: number): any | null {
|
|||||||
if (!prev || prev.role !== 'user') {
|
if (!prev || prev.role !== 'user') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// assistant 头部属于“紧挨着它上方的一组连续 user 消息”。运行期自动插入
|
// assistant 头部属于“紧挨着它上方的一组连续 user-side 消息”。
|
||||||
// guidance/goal 等 user 消息时,头部仍要显示在整组 user 消息下方,并沿用
|
// 只有 starts_work=true 的消息才会开启新 work segment;运行期 guidance
|
||||||
// 这组里真正触发工作的那条 user 消息的计时器。
|
// 只是当前 segment 内的补充,不会重置头部与计时器。
|
||||||
for (let i = index - 1; i >= 0; i -= 1) {
|
for (let i = index - 1; i >= 0; i -= 1) {
|
||||||
const item = filteredMessages.value[i];
|
const item = filteredMessages.value[i];
|
||||||
if (!item || item.role !== 'user') {
|
if (!item || item.role !== 'user') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const source = String(item?.metadata?.message_source || 'user')
|
if (messageStartsWork(item)) {
|
||||||
.trim()
|
|
||||||
.toLowerCase();
|
|
||||||
if (['user', 'presend', 'sub_agent', 'background_command'].includes(source)) {
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<aside
|
<aside
|
||||||
class="sidebar right-sidebar tool-approval-panel"
|
class="sidebar right-sidebar tool-approval-panel"
|
||||||
:class="{ collapsed }"
|
:class="{ collapsed, 'is-goal-approval-mode': isGoalApprovalMode }"
|
||||||
:style="{ width: collapsed ? '0px' : width + 'px' }"
|
:style="{ width: collapsed ? '0px' : width + 'px' }"
|
||||||
>
|
>
|
||||||
<div class="sidebar-header" :class="{ 'mobile-header': isMobileViewport }">
|
<div class="sidebar-header" :class="{ 'mobile-header': isMobileViewport }">
|
||||||
@ -103,7 +103,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="autoApprovalFeedLines.length || autoApprovalFinalMessage" class="auto-approval-block">
|
<div
|
||||||
|
v-if="autoApprovalFeedLines.length || autoApprovalFinalMessage"
|
||||||
|
class="auto-approval-block"
|
||||||
|
:class="{ 'auto-approval-block--goal': isGoalApprovalMode }"
|
||||||
|
>
|
||||||
<div v-if="!isGoalApprovalMode" class="auto-approval-block__title">{{ autoApprovalTitle }}</div>
|
<div v-if="!isGoalApprovalMode" class="auto-approval-block__title">{{ autoApprovalTitle }}</div>
|
||||||
<pre class="auto-approval-block__content">{{ autoApprovalFeedLines.join('\n') }}</pre>
|
<pre class="auto-approval-block__content">{{ autoApprovalFeedLines.join('\n') }}</pre>
|
||||||
<div v-if="autoApprovalFinalMessage" class="auto-approval-block__final">
|
<div v-if="autoApprovalFinalMessage" class="auto-approval-block__final">
|
||||||
@ -288,6 +292,22 @@ const parseFinalMessage = (text: string) => {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-approval-panel.is-goal-approval-mode .auto-approval-block {
|
||||||
|
background: var(--theme-surface-card, var(--theme-surface-soft));
|
||||||
|
border-color: var(--theme-control-border, var(--claude-border));
|
||||||
|
color: var(--claude-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-approval-panel.is-goal-approval-mode .auto-approval-block__content,
|
||||||
|
.tool-approval-panel.is-goal-approval-mode .auto-approval-block__reason {
|
||||||
|
color: var(--claude-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-approval-panel.is-goal-approval-mode .auto-approval-block__decision {
|
||||||
|
color: var(--claude-text);
|
||||||
|
}
|
||||||
|
|
||||||
:global(:root[data-theme='dark']) .auto-approval-block,
|
:global(:root[data-theme='dark']) .auto-approval-block,
|
||||||
:global(body[data-theme='dark']) .auto-approval-block {
|
:global(body[data-theme='dark']) .auto-approval-block {
|
||||||
background: color-mix(in srgb, var(--theme-surface-muted, #141414) 86%, white 4%);
|
background: color-mix(in srgb, var(--theme-surface-muted, #141414) 86%, white 4%);
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
import { io as createSocketClient } from 'socket.io-client';
|
import { io as createSocketClient } from 'socket.io-client';
|
||||||
import { renderLatexInRealtime } from './useMarkdownRenderer';
|
import { renderLatexInRealtime } from './useMarkdownRenderer';
|
||||||
|
import { getMessageVisibility, messageStartsWork } from '../utils/messageVisibility';
|
||||||
|
|
||||||
export async function initializeLegacySocket(ctx: any) {
|
export async function initializeLegacySocket(ctx: any) {
|
||||||
try {
|
try {
|
||||||
@ -910,6 +911,36 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
: Array.isArray(data.mediaRefs)
|
: Array.isArray(data.mediaRefs)
|
||||||
? data.mediaRefs
|
? data.mediaRefs
|
||||||
: [];
|
: [];
|
||||||
|
const source = String(data?.message_source || data?.metadata?.message_source || 'user')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
const eventMetadata = {
|
||||||
|
...(data?.metadata || {}),
|
||||||
|
message_source: source
|
||||||
|
};
|
||||||
|
if (Object.prototype.hasOwnProperty.call(data || {}, 'visibility')) {
|
||||||
|
eventMetadata.visibility = data.visibility;
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(data || {}, 'starts_work')) {
|
||||||
|
eventMetadata.starts_work = data.starts_work;
|
||||||
|
}
|
||||||
|
eventMetadata.visibility = getMessageVisibility({ role: 'user', content: message, metadata: eventMetadata, ...data });
|
||||||
|
eventMetadata.starts_work = messageStartsWork({ role: 'user', content: message, metadata: eventMetadata, ...data });
|
||||||
|
const isAutoUserMessage = !!(
|
||||||
|
data?.is_auto_generated ||
|
||||||
|
data?.metadata?.is_auto_generated ||
|
||||||
|
data?.auto_message_type ||
|
||||||
|
data?.metadata?.auto_message_type ||
|
||||||
|
data?.sub_agent_notice ||
|
||||||
|
data?.background_command_notice
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
isAutoUserMessage &&
|
||||||
|
eventMetadata.starts_work === true &&
|
||||||
|
typeof ctx.markLatestUserWorkCompleted === 'function'
|
||||||
|
) {
|
||||||
|
ctx.markLatestUserWorkCompleted();
|
||||||
|
}
|
||||||
const last =
|
const last =
|
||||||
Array.isArray(ctx.messages) && ctx.messages.length
|
Array.isArray(ctx.messages) && ctx.messages.length
|
||||||
? ctx.messages[ctx.messages.length - 1]
|
? ctx.messages[ctx.messages.length - 1]
|
||||||
@ -925,10 +956,11 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
last.media_refs = incomingMediaRefs;
|
last.media_refs = incomingMediaRefs;
|
||||||
last.metadata = {
|
last.metadata = {
|
||||||
...(last.metadata || {}),
|
...(last.metadata || {}),
|
||||||
|
...eventMetadata,
|
||||||
media_refs: incomingMediaRefs
|
media_refs: incomingMediaRefs
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
ctx.chatAddUserMessage(message, incomingImages, incomingVideos, incomingMediaRefs);
|
ctx.chatAddUserMessage(message, incomingImages, incomingVideos, incomingMediaRefs, source, eventMetadata);
|
||||||
}
|
}
|
||||||
ctx.taskInProgress = true;
|
ctx.taskInProgress = true;
|
||||||
ctx.streamingMessage = false;
|
ctx.streamingMessage = false;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
import { getMessageVisibility, messageStartsWork } from '@/utils/messageVisibility';
|
||||||
|
|
||||||
interface ScrollStatePayload {
|
interface ScrollStatePayload {
|
||||||
autoScrollEnabled?: boolean;
|
autoScrollEnabled?: boolean;
|
||||||
@ -184,17 +185,19 @@ export const useChatStore = defineStore('chat', {
|
|||||||
images: string[] = [],
|
images: string[] = [],
|
||||||
videos: string[] = [],
|
videos: string[] = [],
|
||||||
mediaRefs: Array<Record<string, any>> = [],
|
mediaRefs: Array<Record<string, any>> = [],
|
||||||
source: string = 'user'
|
source: string = 'user',
|
||||||
|
extraMetadata: Record<string, any> = {}
|
||||||
) {
|
) {
|
||||||
const startedAt = new Date().toISOString();
|
const startedAt = new Date().toISOString();
|
||||||
const normalizedSource = String(source || 'user').trim().toLowerCase();
|
const normalizedSource = String(source || 'user').trim().toLowerCase();
|
||||||
const shouldTrackWorkTimer = ['user', 'presend', 'sub_agent', 'background_command'].includes(
|
|
||||||
normalizedSource
|
|
||||||
);
|
|
||||||
const metadata: Record<string, any> = {
|
const metadata: Record<string, any> = {
|
||||||
media_refs: Array.isArray(mediaRefs) ? mediaRefs : [],
|
media_refs: Array.isArray(mediaRefs) ? mediaRefs : [],
|
||||||
message_source: normalizedSource
|
message_source: normalizedSource,
|
||||||
|
...extraMetadata
|
||||||
};
|
};
|
||||||
|
metadata.visibility = getMessageVisibility({ role: 'user', content, metadata });
|
||||||
|
metadata.starts_work = messageStartsWork({ role: 'user', content, metadata });
|
||||||
|
const shouldTrackWorkTimer = metadata.starts_work === true;
|
||||||
if (shouldTrackWorkTimer) {
|
if (shouldTrackWorkTimer) {
|
||||||
metadata.work_timer = {
|
metadata.work_timer = {
|
||||||
status: 'working',
|
status: 'working',
|
||||||
|
|||||||
@ -457,6 +457,10 @@
|
|||||||
align-items: flex-end; /* 靠右对齐 */
|
align-items: flex-end; /* 靠右对齐 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-message.user-message--compact {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.user-message .message-header,
|
.user-message .message-header,
|
||||||
.assistant-message .message-header {
|
.assistant-message .message-header {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@ -496,6 +500,17 @@
|
|||||||
align-self: flex-end; /* 确保靠右 */
|
align-self: flex-end; /* 确保靠右 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-message.user-message--compact .message-text {
|
||||||
|
max-width: min(760px, 88%);
|
||||||
|
align-self: center;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--claude-text-secondary);
|
||||||
|
background: var(--theme-surface-card);
|
||||||
|
border: 1px solid var(--theme-control-border);
|
||||||
|
}
|
||||||
|
|
||||||
.user-message .message-text .user-skill-link {
|
.user-message .message-text .user-skill-link {
|
||||||
color: #2563eb;
|
color: #2563eb;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
79
static/src/utils/messageVisibility.ts
Normal file
79
static/src/utils/messageVisibility.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
export type MessageVisibility = 'chat' | 'compact' | 'hidden';
|
||||||
|
|
||||||
|
const VALID_VISIBILITY = new Set(['chat', 'compact', 'hidden']);
|
||||||
|
const LEGACY_STARTS_WORK_SOURCES = new Set(['user', 'presend', 'sub_agent', 'background_command']);
|
||||||
|
const COMPACT_FALLBACK_SOURCES = new Set([
|
||||||
|
'guidance',
|
||||||
|
'goal_review',
|
||||||
|
'compression',
|
||||||
|
'sub_agent',
|
||||||
|
'background_command'
|
||||||
|
]);
|
||||||
|
|
||||||
|
function normalizeSource(value: any): string {
|
||||||
|
return String(value || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function messageContent(message: any): string {
|
||||||
|
return String(message?.content || message?.message || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLegacyGoalPrompt(message: any): boolean {
|
||||||
|
const content = messageContent(message);
|
||||||
|
return content.includes('【目标模式已开启】') || content.includes('[系统通知|goal]\n【目标模式已开启】');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLegacyGoalReview(message: any): boolean {
|
||||||
|
const content = messageContent(message);
|
||||||
|
return content.includes('审核智能体对于你的工作结束给出了以下内容');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMessageVisibility(message: any): MessageVisibility {
|
||||||
|
const meta = message?.metadata || {};
|
||||||
|
const explicit = normalizeSource(meta.visibility || meta.ui?.visibility || message?.visibility);
|
||||||
|
if (VALID_VISIBILITY.has(explicit)) {
|
||||||
|
return explicit as MessageVisibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta.hidden === true || meta.system_injected_image || meta.system_injected_video) {
|
||||||
|
return 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = normalizeSource(meta.message_source || meta.source || message?.message_source || message?.source);
|
||||||
|
if (source === 'skill' || source === 'goal_prompt' || isLegacyGoalPrompt(message)) {
|
||||||
|
return 'hidden';
|
||||||
|
}
|
||||||
|
if (source === 'notify' && (meta.runtime_mode_notice || message?.runtime_mode_notice)) {
|
||||||
|
return 'hidden';
|
||||||
|
}
|
||||||
|
if (COMPACT_FALLBACK_SOURCES.has(source) || isLegacyGoalReview(message)) {
|
||||||
|
return 'compact';
|
||||||
|
}
|
||||||
|
return 'chat';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function messageStartsWork(message: any): boolean {
|
||||||
|
const meta = message?.metadata || {};
|
||||||
|
if (typeof meta.starts_work === 'boolean') {
|
||||||
|
return meta.starts_work;
|
||||||
|
}
|
||||||
|
if (typeof meta.ui?.starts_work === 'boolean') {
|
||||||
|
return meta.ui.starts_work;
|
||||||
|
}
|
||||||
|
if (typeof message?.starts_work === 'boolean') {
|
||||||
|
return message.starts_work;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getMessageVisibility(message) === 'hidden') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = normalizeSource(meta.message_source || meta.source || message?.message_source || message?.source);
|
||||||
|
if (source === 'guidance' || source === 'goal_prompt' || source === 'skill') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (source === 'goal_review' || source === 'compression' || isLegacyGoalReview(message)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return LEGACY_STARTS_WORK_SOURCES.has(source || 'user');
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user