fix(ui): refine message visibility and goal approval flow

This commit is contained in:
JOJO 2026-05-30 16:30:05 +08:00
parent e220b3703a
commit 32865f6d6e
10 changed files with 320 additions and 44 deletions

View File

@ -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]}")

View File

@ -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,
} }

View File

@ -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,

View File

@ -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) {

View File

@ -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;
} }
} }

View File

@ -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%);

View File

@ -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;

View File

@ -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',

View File

@ -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;

View 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');
}