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,
|
||||
)
|
||||
|
||||
_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(
|
||||
*,
|
||||
@ -308,7 +339,7 @@ async def _dispatch_completion_user_notice(
|
||||
message_source = "background_command"
|
||||
elif extra_payload.get("sub_agent_notice"):
|
||||
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"
|
||||
try:
|
||||
from .tasks import task_manager
|
||||
@ -326,10 +357,17 @@ async def _dispatch_completion_user_notice(
|
||||
"model_key": getattr(web_terminal, "model_key", None),
|
||||
"message_source": message_source,
|
||||
}
|
||||
ui_defaults = _user_message_ui_defaults(
|
||||
message_source,
|
||||
auto_user_message_event=True,
|
||||
)
|
||||
# 关键:通知类后台任务需要把 user_message 写入任务事件流,
|
||||
# 否则前端轮询只会看到 AI/tool 事件,看不到 user_message。
|
||||
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(
|
||||
username,
|
||||
workspace_id,
|
||||
@ -346,8 +384,14 @@ async def _dispatch_completion_user_notice(
|
||||
'conversation_id': conversation_id,
|
||||
'task_id': rec.task_id,
|
||||
'message_source': message_source,
|
||||
**ui_defaults,
|
||||
}
|
||||
payload.update(extra_payload)
|
||||
payload["metadata"] = {
|
||||
"message_source": message_source,
|
||||
**ui_defaults,
|
||||
**(payload.get("metadata") or {}),
|
||||
}
|
||||
sender('user_message', payload)
|
||||
return
|
||||
except Exception as e:
|
||||
@ -358,7 +402,14 @@ async def _dispatch_completion_user_notice(
|
||||
'conversation_id': conversation_id,
|
||||
'message_source': message_source,
|
||||
}
|
||||
payload.update(_user_message_ui_defaults(message_source, auto_user_message_event=True))
|
||||
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)
|
||||
try:
|
||||
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_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 [])
|
||||
is_first_user_message = history_len_before == 0
|
||||
# 构建 user 消息来源与 metadata
|
||||
@ -809,14 +861,20 @@ async def handle_task_with_sender(
|
||||
user_message_source = "background_command"
|
||||
elif auto_payload.get("sub_agent_notice"):
|
||||
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消息metadata
|
||||
user_message_metadata = {
|
||||
"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"] = {
|
||||
"status": "working",
|
||||
"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
|
||||
except Exception:
|
||||
user_message_index = -1
|
||||
current_work_user_message_index = user_message_index
|
||||
skill_context_messages = getattr(web_terminal, "_skill_context_messages", None)
|
||||
if not auto_user_message_event and isinstance(skill_context_messages, list):
|
||||
for skill_item in skill_context_messages:
|
||||
@ -852,6 +911,8 @@ async def handle_task_with_sender(
|
||||
metadata={
|
||||
"source": "skill",
|
||||
"message_source": "skill",
|
||||
"visibility": "hidden",
|
||||
"starts_work": False,
|
||||
"hidden": True,
|
||||
"skill_name": skill_name,
|
||||
"skill_path": skill_path,
|
||||
@ -867,6 +928,9 @@ async def handle_task_with_sender(
|
||||
"videos": (saved_user_message or {}).get("videos") or videos or [],
|
||||
"media_refs": (saved_user_message or {}).get("media_refs") or [],
|
||||
"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,
|
||||
},
|
||||
)
|
||||
@ -880,14 +944,14 @@ async def handle_task_with_sender(
|
||||
auto_user_message_event=bool(auto_user_message_event),
|
||||
)
|
||||
|
||||
def finalize_user_work_timer():
|
||||
nonlocal user_work_finalized
|
||||
if user_work_finalized:
|
||||
def finalize_user_work_timer(index: Optional[int] = None):
|
||||
target_index = current_work_user_message_index if index is None else index
|
||||
if target_index in finalized_work_message_indices:
|
||||
return
|
||||
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
|
||||
target_msg = history[user_message_index] or {}
|
||||
target_msg = history[target_index] or {}
|
||||
if target_msg.get("role") != "user":
|
||||
return
|
||||
metadata = target_msg.get("metadata") or {}
|
||||
@ -910,9 +974,20 @@ async def handle_task_with_sender(
|
||||
})
|
||||
metadata["work_timer"] = timer
|
||||
target_msg["metadata"] = metadata
|
||||
history[user_message_index] = target_msg
|
||||
history[target_index] = target_msg
|
||||
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
|
||||
|
||||
@ -1394,7 +1469,10 @@ async def handle_task_with_sender(
|
||||
)
|
||||
action = goal_result.get("action")
|
||||
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
|
||||
is_first_iteration = False
|
||||
debug_log(f"[Goal] 续命:{goal_result.get('message', '')[:80]}")
|
||||
|
||||
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from modules.sub_agent_manager import TERMINAL_STATUSES
|
||||
@ -15,9 +16,31 @@ _VALID_SOURCES = {
|
||||
"sub_agent",
|
||||
"background_command",
|
||||
"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(
|
||||
*,
|
||||
web_terminal,
|
||||
@ -57,8 +80,14 @@ def inject_runtime_user_message(
|
||||
"runtime_guidance": True,
|
||||
"runtime_guidance_original": raw,
|
||||
}
|
||||
metadata.update(_runtime_message_ui_defaults(src, inline=inline))
|
||||
if extra_metadata:
|
||||
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:
|
||||
try:
|
||||
@ -93,6 +122,9 @@ def inject_runtime_user_message(
|
||||
"inline": inline,
|
||||
"source": src,
|
||||
"message_source": src,
|
||||
"visibility": metadata.get("visibility"),
|
||||
"starts_work": metadata.get("starts_work"),
|
||||
"metadata": metadata,
|
||||
"runtime_guidance": True,
|
||||
"runtime_guidance_original": raw,
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ def inject_goal_prompt(
|
||||
web_terminal=web_terminal,
|
||||
messages=messages,
|
||||
text=GOAL_MODE_PROMPT,
|
||||
source="goal",
|
||||
source="goal_prompt",
|
||||
sender=sender,
|
||||
conversation_id=conversation_id,
|
||||
inline=False,
|
||||
@ -231,7 +231,7 @@ async def handle_goal_after_turn(
|
||||
web_terminal=web_terminal,
|
||||
messages=messages,
|
||||
text=f"{CONTINUE_PREFIX}{message}",
|
||||
source="goal",
|
||||
source="goal_review",
|
||||
sender=sender,
|
||||
conversation_id=conversation_id,
|
||||
inline=False,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import { debugLog } from './common';
|
||||
import { useTaskStore } from '../../stores/task';
|
||||
import { getMessageVisibility, messageStartsWork } from '../../utils/messageVisibility';
|
||||
|
||||
const debugNotifyLog = (...args: any[]) => {
|
||||
void args;
|
||||
@ -114,6 +115,23 @@ function resolveUserMessageSource(data: any): string {
|
||||
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 {
|
||||
if (!message || message.role !== 'assistant') {
|
||||
return false;
|
||||
@ -1912,10 +1930,6 @@ export const taskPollingMethods = {
|
||||
this.autoApprovalFeedLines.push(String(progress.message));
|
||||
}
|
||||
this.autoApprovalFeedLines = this.autoApprovalFeedLines.slice(-20);
|
||||
this.rightCollapsed = false;
|
||||
if (this.rightWidth < this.minPanelWidth) {
|
||||
this.rightWidth = this.minPanelWidth;
|
||||
}
|
||||
this.$forceUpdate();
|
||||
},
|
||||
|
||||
@ -2044,6 +2058,7 @@ export const taskPollingMethods = {
|
||||
const isAutoUserMessage = isSystemAutoUserMessagePayload(data);
|
||||
const isRuntimeModeNotice = isRuntimeModeNoticePayload(data);
|
||||
const source = resolveUserMessageSource(data);
|
||||
const eventMetadata = resolveUserMessageMetadata(data, source, message);
|
||||
const incomingImages = Array.isArray(data?.images) ? data.images : [];
|
||||
const incomingVideos = Array.isArray(data?.videos) ? data.videos : [];
|
||||
const incomingMediaRefs = Array.isArray(data?.media_refs)
|
||||
@ -2072,6 +2087,7 @@ export const taskPollingMethods = {
|
||||
target.media_refs = incomingMediaRefs;
|
||||
target.metadata = {
|
||||
...(target.metadata || {}),
|
||||
...eventMetadata,
|
||||
media_refs: incomingMediaRefs
|
||||
};
|
||||
} else {
|
||||
@ -2080,7 +2096,8 @@ export const taskPollingMethods = {
|
||||
incomingImages,
|
||||
incomingVideos,
|
||||
incomingMediaRefs,
|
||||
source
|
||||
source,
|
||||
eventMetadata
|
||||
);
|
||||
}
|
||||
this.taskInProgress = true;
|
||||
@ -2099,6 +2116,9 @@ export const taskPollingMethods = {
|
||||
messagePreview: message.slice(0, 80)
|
||||
});
|
||||
} else {
|
||||
if (eventMetadata.starts_work === true) {
|
||||
this.markLatestUserWorkCompleted();
|
||||
}
|
||||
const shouldRestoreWaitingPlaceholder =
|
||||
this.moveTrailingEmptyAssistantPlaceholderAfterUserInsert('auto_user_message') &&
|
||||
(this.taskInProgress || this.streamingMessage);
|
||||
@ -2113,6 +2133,7 @@ export const taskPollingMethods = {
|
||||
recentMatch.media_refs = incomingMediaRefs;
|
||||
recentMatch.metadata = {
|
||||
...(recentMatch.metadata || {}),
|
||||
...eventMetadata,
|
||||
media_refs: incomingMediaRefs,
|
||||
message_source: source
|
||||
};
|
||||
@ -2122,7 +2143,8 @@ export const taskPollingMethods = {
|
||||
incomingImages,
|
||||
incomingVideos,
|
||||
incomingMediaRefs,
|
||||
source
|
||||
source,
|
||||
eventMetadata
|
||||
);
|
||||
}
|
||||
if (shouldRestoreWaitingPlaceholder) {
|
||||
|
||||
@ -6,11 +6,12 @@
|
||||
:key="index"
|
||||
class="message-block"
|
||||
:class="{
|
||||
'message-block--compact-user': getMessageVisibility(msg) === 'compact',
|
||||
'message-block--sub-agent-notice': isSubAgentNoticeOnlyMessage(msg),
|
||||
'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">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe(userHeaderIconKey(msg))" aria-hidden="true"></span>
|
||||
<span>{{ userHeaderLabel(msg) }}</span>
|
||||
@ -596,6 +597,7 @@ import ToolAction from '@/components/chat/actions/ToolAction.vue';
|
||||
import StackedBlocks from './StackedBlocks.vue';
|
||||
import MinimalBlocks from './MinimalBlocks.vue';
|
||||
import { usePersonalizationStore } from '@/stores/personalization';
|
||||
import { getMessageVisibility, messageStartsWork } from '@/utils/messageVisibility';
|
||||
|
||||
const props = defineProps<{
|
||||
messages: Array<any>;
|
||||
@ -677,15 +679,11 @@ function renderUserMessageContent(content: string): string {
|
||||
html += escapeUserHtml(source.slice(cursor));
|
||||
return html;
|
||||
}
|
||||
const isHiddenSkillMessage = (message: any) => {
|
||||
const isHiddenUserMessage = (message: any) => {
|
||||
if (!message || message.role !== 'user') {
|
||||
return false;
|
||||
}
|
||||
const meta = message.metadata || {};
|
||||
const source = String(meta.source || meta.message_source || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return source === 'skill' || meta.hidden === true;
|
||||
return getMessageVisibility(message) === 'hidden';
|
||||
};
|
||||
const isEmptyAssistantMessage = (message: any) => {
|
||||
if (!message || message.role !== 'assistant') {
|
||||
@ -720,7 +718,7 @@ const filteredMessages = computed(() => {
|
||||
if (m?.role === 'system') {
|
||||
return false;
|
||||
}
|
||||
if (isHiddenSkillMessage(m)) {
|
||||
if (isHiddenUserMessage(m)) {
|
||||
return false;
|
||||
}
|
||||
if (isEmptyAssistantMessage(m)) {
|
||||
@ -1353,18 +1351,15 @@ function findAssistantHeaderAnchorUser(index: number): any | null {
|
||||
if (!prev || prev.role !== 'user') {
|
||||
return null;
|
||||
}
|
||||
// assistant 头部属于“紧挨着它上方的一组连续 user 消息”。运行期自动插入
|
||||
// guidance/goal 等 user 消息时,头部仍要显示在整组 user 消息下方,并沿用
|
||||
// 这组里真正触发工作的那条 user 消息的计时器。
|
||||
// assistant 头部属于“紧挨着它上方的一组连续 user-side 消息”。
|
||||
// 只有 starts_work=true 的消息才会开启新 work segment;运行期 guidance
|
||||
// 只是当前 segment 内的补充,不会重置头部与计时器。
|
||||
for (let i = index - 1; i >= 0; i -= 1) {
|
||||
const item = filteredMessages.value[i];
|
||||
if (!item || item.role !== 'user') {
|
||||
break;
|
||||
}
|
||||
const source = String(item?.metadata?.message_source || 'user')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (['user', 'presend', 'sub_agent', 'background_command'].includes(source)) {
|
||||
if (messageStartsWork(item)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<aside
|
||||
class="sidebar right-sidebar tool-approval-panel"
|
||||
:class="{ collapsed }"
|
||||
:class="{ collapsed, 'is-goal-approval-mode': isGoalApprovalMode }"
|
||||
:style="{ width: collapsed ? '0px' : width + 'px' }"
|
||||
>
|
||||
<div class="sidebar-header" :class="{ 'mobile-header': isMobileViewport }">
|
||||
@ -103,7 +103,11 @@
|
||||
</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>
|
||||
<pre class="auto-approval-block__content">{{ autoApprovalFeedLines.join('\n') }}</pre>
|
||||
<div v-if="autoApprovalFinalMessage" class="auto-approval-block__final">
|
||||
@ -288,6 +292,22 @@ const parseFinalMessage = (text: string) => {
|
||||
white-space: pre-wrap;
|
||||
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(body[data-theme='dark']) .auto-approval-block {
|
||||
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 { renderLatexInRealtime } from './useMarkdownRenderer';
|
||||
import { getMessageVisibility, messageStartsWork } from '../utils/messageVisibility';
|
||||
|
||||
export async function initializeLegacySocket(ctx: any) {
|
||||
try {
|
||||
@ -910,6 +911,36 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
: Array.isArray(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 =
|
||||
Array.isArray(ctx.messages) && ctx.messages.length
|
||||
? ctx.messages[ctx.messages.length - 1]
|
||||
@ -925,10 +956,11 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
last.media_refs = incomingMediaRefs;
|
||||
last.metadata = {
|
||||
...(last.metadata || {}),
|
||||
...eventMetadata,
|
||||
media_refs: incomingMediaRefs
|
||||
};
|
||||
} else {
|
||||
ctx.chatAddUserMessage(message, incomingImages, incomingVideos, incomingMediaRefs);
|
||||
ctx.chatAddUserMessage(message, incomingImages, incomingVideos, incomingMediaRefs, source, eventMetadata);
|
||||
}
|
||||
ctx.taskInProgress = true;
|
||||
ctx.streamingMessage = false;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { getMessageVisibility, messageStartsWork } from '@/utils/messageVisibility';
|
||||
|
||||
interface ScrollStatePayload {
|
||||
autoScrollEnabled?: boolean;
|
||||
@ -184,17 +185,19 @@ export const useChatStore = defineStore('chat', {
|
||||
images: string[] = [],
|
||||
videos: string[] = [],
|
||||
mediaRefs: Array<Record<string, any>> = [],
|
||||
source: string = 'user'
|
||||
source: string = 'user',
|
||||
extraMetadata: Record<string, any> = {}
|
||||
) {
|
||||
const startedAt = new Date().toISOString();
|
||||
const normalizedSource = String(source || 'user').trim().toLowerCase();
|
||||
const shouldTrackWorkTimer = ['user', 'presend', 'sub_agent', 'background_command'].includes(
|
||||
normalizedSource
|
||||
);
|
||||
const metadata: Record<string, any> = {
|
||||
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) {
|
||||
metadata.work_timer = {
|
||||
status: 'working',
|
||||
|
||||
@ -457,6 +457,10 @@
|
||||
align-items: flex-end; /* 靠右对齐 */
|
||||
}
|
||||
|
||||
.user-message.user-message--compact {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-message .message-header,
|
||||
.assistant-message .message-header {
|
||||
font-size: 14px;
|
||||
@ -496,6 +500,17 @@
|
||||
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 {
|
||||
color: #2563eb;
|
||||
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