feat(chat): add compact message display mode and goal-review/compression headers
- 审核(goal_review) user 消息气泡显示「审核 + circle-alert 图标」 - compact 消息新增「简略信息」显示形式(个人空间可切换),渲染为横线包裹的一行概要 - 压缩续接消息单独标记为 compression_handoff,visibility 改为 compact Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
32865f6d6e
commit
f5e4541d83
@ -152,6 +152,7 @@ _VALID_USER_MESSAGE_SOURCES = {
|
||||
"goal_prompt",
|
||||
"goal_review",
|
||||
"compression",
|
||||
"compression_handoff",
|
||||
"permission",
|
||||
"sandbox",
|
||||
"skill",
|
||||
@ -164,7 +165,7 @@ def _user_message_ui_defaults(source: str, *, auto_user_message_event: bool = Fa
|
||||
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"}:
|
||||
if normalized in {"goal_review", "compression", "compression_handoff", "sub_agent", "background_command"}:
|
||||
return {"visibility": "compact", "starts_work": True}
|
||||
if normalized == "presend":
|
||||
return {"visibility": "chat", "starts_work": True}
|
||||
@ -1085,16 +1086,22 @@ async def handle_task_with_sender(
|
||||
if guide_message:
|
||||
finalize_user_work_timer()
|
||||
finalize_run_versioning_checkpoint("auto_deep_compress_handoff")
|
||||
await handle_task_with_sender(
|
||||
web_terminal,
|
||||
workspace,
|
||||
guide_message,
|
||||
[],
|
||||
sender,
|
||||
client_sid,
|
||||
username,
|
||||
[]
|
||||
)
|
||||
# 压缩后的续接引导语单独标记来源,使其以 compact 形式展示,
|
||||
# 而不是混入普通用户消息流。
|
||||
setattr(web_terminal, "_current_user_message_source", "compression_handoff")
|
||||
try:
|
||||
await handle_task_with_sender(
|
||||
web_terminal,
|
||||
workspace,
|
||||
guide_message,
|
||||
[],
|
||||
sender,
|
||||
client_sid,
|
||||
username,
|
||||
[]
|
||||
)
|
||||
finally:
|
||||
setattr(web_terminal, "_current_user_message_source", "user")
|
||||
return
|
||||
finalize_user_work_timer()
|
||||
finalize_run_versioning_checkpoint("auto_deep_compress_end")
|
||||
@ -1570,16 +1577,22 @@ async def handle_task_with_sender(
|
||||
if deep_result.get("success") and guide_message:
|
||||
finalize_user_work_timer()
|
||||
finalize_run_versioning_checkpoint("deep_compress_handoff")
|
||||
await handle_task_with_sender(
|
||||
web_terminal,
|
||||
workspace,
|
||||
guide_message,
|
||||
[],
|
||||
sender,
|
||||
client_sid,
|
||||
username,
|
||||
[]
|
||||
)
|
||||
# 压缩后的续接引导语单独标记来源,使其以 compact 形式展示,
|
||||
# 而不是混入普通用户消息流。
|
||||
setattr(web_terminal, "_current_user_message_source", "compression_handoff")
|
||||
try:
|
||||
await handle_task_with_sender(
|
||||
web_terminal,
|
||||
workspace,
|
||||
guide_message,
|
||||
[],
|
||||
sender,
|
||||
client_sid,
|
||||
username,
|
||||
[]
|
||||
)
|
||||
finally:
|
||||
setattr(web_terminal, "_current_user_message_source", "user")
|
||||
return
|
||||
finalize_user_work_timer()
|
||||
finalize_run_versioning_checkpoint("deep_compress_end")
|
||||
|
||||
@ -19,6 +19,7 @@ _VALID_SOURCES = {
|
||||
"goal_prompt",
|
||||
"goal_review",
|
||||
"compression",
|
||||
"compression_handoff",
|
||||
"permission",
|
||||
"sandbox",
|
||||
"skill",
|
||||
@ -32,7 +33,7 @@ def _runtime_message_ui_defaults(src: str, *, inline: bool = False) -> Dict[str,
|
||||
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"}:
|
||||
if normalized in {"goal_review", "compression", "compression_handoff", "sub_agent", "background_command", "presend"}:
|
||||
return {"visibility": "compact", "starts_work": True}
|
||||
if normalized == "goal":
|
||||
# Backward-compatible fallback for old callers. Inline goal injections are
|
||||
|
||||
@ -1207,6 +1207,7 @@ def compress_conversation(conversation_id, terminal: WebTerminal, workspace: Use
|
||||
images=[],
|
||||
conversation_id=new_conversation_id,
|
||||
videos=[],
|
||||
message_source="compression_handoff",
|
||||
)
|
||||
response_payload["auto_task_started"] = True
|
||||
response_payload["auto_task_id"] = task_rec.task_id
|
||||
|
||||
@ -11,44 +11,49 @@
|
||||
'message-block--before-sub-agent-notice': isFollowedBySubAgentNotice(index)
|
||||
}"
|
||||
>
|
||||
<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>
|
||||
<div v-if="msg.role === 'user'" class="user-message" :class="{ 'user-message--compact': getMessageVisibility(msg) === 'compact', 'user-message--brief': isBriefCompactMessage(msg) }">
|
||||
<div v-if="isBriefCompactMessage(msg)" class="compact-brief-line" role="separator">
|
||||
<span class="compact-brief-text">{{ compactBriefLabel(msg) }}</span>
|
||||
</div>
|
||||
<div class="message-text user-bubble-text">
|
||||
<div
|
||||
v-if="msg.content"
|
||||
class="bubble-text"
|
||||
v-html="renderUserMessageContent(msg.content)"
|
||||
></div>
|
||||
<div v-if="msg.images && msg.images.length" class="image-inline-row">
|
||||
<template v-else>
|
||||
<div class="message-header icon-label">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe(userHeaderIconKey(msg))" aria-hidden="true"></span>
|
||||
<span>{{ userHeaderLabel(msg) }}</span>
|
||||
</div>
|
||||
<div class="message-text user-bubble-text">
|
||||
<div
|
||||
class="image-thumbnail-wrapper"
|
||||
v-for="(img, imgIndex) in msg.images"
|
||||
:key="mediaPreviewKey(msg, img, imgIndex)"
|
||||
>
|
||||
<img
|
||||
:src="getPreviewUrl(msg, img, 'image')"
|
||||
:alt="formatImageName(img)"
|
||||
class="image-thumbnail"
|
||||
/>
|
||||
v-if="msg.content"
|
||||
class="bubble-text"
|
||||
v-html="renderUserMessageContent(msg.content)"
|
||||
></div>
|
||||
<div v-if="msg.images && msg.images.length" class="image-inline-row">
|
||||
<div
|
||||
class="image-thumbnail-wrapper"
|
||||
v-for="(img, imgIndex) in msg.images"
|
||||
:key="mediaPreviewKey(msg, img, imgIndex)"
|
||||
>
|
||||
<img
|
||||
:src="getPreviewUrl(msg, img, 'image')"
|
||||
:alt="formatImageName(img)"
|
||||
class="image-thumbnail"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="msg.videos && msg.videos.length" class="image-inline-row video-inline-row">
|
||||
<div
|
||||
class="image-thumbnail-wrapper"
|
||||
v-for="(video, videoIndex) in msg.videos"
|
||||
:key="mediaPreviewKey(msg, video, videoIndex)"
|
||||
>
|
||||
<img
|
||||
:src="getPreviewUrl(msg, video, 'video')"
|
||||
:alt="formatImageName(video)"
|
||||
class="image-thumbnail"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="msg.videos && msg.videos.length" class="image-inline-row video-inline-row">
|
||||
<div
|
||||
class="image-thumbnail-wrapper"
|
||||
v-for="(video, videoIndex) in msg.videos"
|
||||
:key="mediaPreviewKey(msg, video, videoIndex)"
|
||||
>
|
||||
<img
|
||||
:src="getPreviewUrl(msg, video, 'video')"
|
||||
:alt="formatImageName(video)"
|
||||
class="image-thumbnail"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="msg.role === 'assistant'" class="assistant-message">
|
||||
<!-- 在 assistant 消息前显示 AI Assistant 头部 -->
|
||||
@ -627,6 +632,9 @@ const personalization = usePersonalizationStore();
|
||||
const blockDisplayMode = computed(() => {
|
||||
return personalization.experiments.blockDisplayMode || 'stacked';
|
||||
});
|
||||
const compactMessageDisplay = computed(() => {
|
||||
return personalization.experiments.compactMessageDisplay || 'full';
|
||||
});
|
||||
const stackedBlocksEnabled = computed(() => {
|
||||
// 堆叠模式和极简模式都使用堆叠布局
|
||||
return blockDisplayMode.value === 'stacked' || blockDisplayMode.value === 'minimal';
|
||||
@ -1380,6 +1388,12 @@ function userHeaderLabel(msg: any): string {
|
||||
if (source === 'goal') {
|
||||
return '目标';
|
||||
}
|
||||
if (source === 'goal_review') {
|
||||
return '审核';
|
||||
}
|
||||
if (source === 'compression' || source === 'compression_handoff') {
|
||||
return '压缩';
|
||||
}
|
||||
if (source === 'notify' || source === 'sub_agent' || source === 'background_command') {
|
||||
return '通知';
|
||||
}
|
||||
@ -1394,12 +1408,54 @@ function userHeaderIconKey(msg: any): string {
|
||||
if (source === 'goal') {
|
||||
return 'flag';
|
||||
}
|
||||
if (source === 'goal_review') {
|
||||
return 'circleAlert';
|
||||
}
|
||||
if (source === 'compression' || source === 'compression_handoff') {
|
||||
return 'layers';
|
||||
}
|
||||
if (source === 'notify' || source === 'sub_agent' || source === 'background_command') {
|
||||
return 'bell';
|
||||
}
|
||||
return 'user';
|
||||
}
|
||||
|
||||
// ---- 简略信息渲染(compact 消息可选显示形式)----
|
||||
// 命中条件:当前消息为 compact 可见性,且用户在个人空间选择了“简略信息”。
|
||||
function isBriefCompactMessage(msg: any): boolean {
|
||||
if (!msg || msg.role !== 'user') {
|
||||
return false;
|
||||
}
|
||||
if (compactMessageDisplay.value !== 'brief') {
|
||||
return false;
|
||||
}
|
||||
return getMessageVisibility(msg) === 'compact';
|
||||
}
|
||||
|
||||
// 简略信息只展示一行概要标签(不显示原始内容),按来源给出固定文案。
|
||||
function compactBriefLabel(msg: any): string {
|
||||
const source = userHeaderSource(msg);
|
||||
switch (source) {
|
||||
case 'goal_review':
|
||||
return '目标审核完成';
|
||||
case 'sub_agent':
|
||||
return '后台子智能体完成';
|
||||
case 'background_command':
|
||||
return '后台指令完成';
|
||||
case 'compression':
|
||||
case 'compression_handoff':
|
||||
return '对话压缩完成';
|
||||
case 'goal':
|
||||
return '目标进行中';
|
||||
case 'guidance':
|
||||
return '运行中引导';
|
||||
case 'notify':
|
||||
return '系统通知';
|
||||
default:
|
||||
return '系统消息';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
attachBounceListener();
|
||||
console.warn('[SCROLL_BOUNCE_TRACE]', 'mounted', {
|
||||
|
||||
@ -615,6 +615,45 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-select-row">
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">简略消息显示</span
|
||||
><span class="settings-row-desc"
|
||||
>审核、子智能体等紧凑系统消息的显示形式。</span
|
||||
></span
|
||||
>
|
||||
<div
|
||||
class="settings-select-wrap"
|
||||
:class="{ open: activeDropdown === 'compact-message' }"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="settings-select-button"
|
||||
@click="toggleDropdown('compact-message')"
|
||||
>
|
||||
{{ compactMessageDisplayLabel }}
|
||||
<span class="select-chevron" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div
|
||||
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||
>
|
||||
<button
|
||||
v-for="option in compactMessageDisplayOptions"
|
||||
:key="option.id"
|
||||
type="button"
|
||||
class="settings-menu-option"
|
||||
:class="{ selected: currentCompactMessageDisplay === option.value }"
|
||||
@click="selectCompactMessageDisplay(option.value)"
|
||||
>
|
||||
<strong>{{ option.label }}</strong
|
||||
><span>{{ option.desc }}</span
|
||||
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
@ -1656,6 +1695,16 @@ const blockDisplayLabel = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const currentCompactMessageDisplay = computed(() => experiments.value.compactMessageDisplay);
|
||||
|
||||
const compactMessageDisplayLabel = computed(() => {
|
||||
return (
|
||||
compactMessageDisplayOptions.find(
|
||||
(option) => option.value === currentCompactMessageDisplay.value
|
||||
)?.label || '完整信息'
|
||||
);
|
||||
});
|
||||
|
||||
const usageSummary = ref({
|
||||
total_input_tokens: 0,
|
||||
total_output_tokens: 0,
|
||||
@ -2084,6 +2133,26 @@ const handleBlockDisplayModeChange = (mode: 'traditional' | 'stacked' | 'minimal
|
||||
personalization.setBlockDisplayMode(mode);
|
||||
};
|
||||
|
||||
const compactMessageDisplayOptions = [
|
||||
{
|
||||
id: 'full',
|
||||
label: '完整信息',
|
||||
desc: '审核、子智能体等系统消息显示完整原始内容',
|
||||
value: 'full' as const
|
||||
},
|
||||
{
|
||||
id: 'brief',
|
||||
label: '简略信息',
|
||||
desc: '仅以一行横线概要替代(如「目标审核完成」)',
|
||||
value: 'brief' as const
|
||||
}
|
||||
];
|
||||
|
||||
const selectCompactMessageDisplay = (mode: 'full' | 'brief') => {
|
||||
personalization.setCompactMessageDisplay(mode);
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const openAdminPanel = () => {
|
||||
window.open('/admin/monitor', '_blank', 'noopener');
|
||||
personalization.closeDrawer();
|
||||
|
||||
@ -2,6 +2,7 @@ import { defineStore } from 'pinia';
|
||||
import { useModelStore } from './model';
|
||||
|
||||
export type BlockDisplayMode = 'traditional' | 'stacked' | 'minimal';
|
||||
export type CompactMessageDisplay = 'full' | 'brief';
|
||||
type RunMode = 'fast' | 'thinking' | 'deep';
|
||||
type PermissionMode = 'readonly' | 'approval' | 'auto_approval' | 'unrestricted';
|
||||
|
||||
@ -53,6 +54,7 @@ interface PersonalForm {
|
||||
|
||||
interface ExperimentState {
|
||||
blockDisplayMode: BlockDisplayMode;
|
||||
compactMessageDisplay: CompactMessageDisplay;
|
||||
}
|
||||
|
||||
interface PersonalizationState {
|
||||
@ -142,7 +144,8 @@ const defaultForm = (): PersonalForm => ({
|
||||
});
|
||||
|
||||
const defaultExperimentState = (): ExperimentState => ({
|
||||
blockDisplayMode: 'stacked'
|
||||
blockDisplayMode: 'stacked',
|
||||
compactMessageDisplay: 'full'
|
||||
});
|
||||
|
||||
const loadExperimentState = (): ExperimentState => {
|
||||
@ -168,8 +171,18 @@ const loadExperimentState = (): ExperimentState => {
|
||||
blockDisplayMode = parsed.stackedBlocksEnabled ? 'stacked' : 'traditional';
|
||||
}
|
||||
|
||||
let compactMessageDisplay: CompactMessageDisplay =
|
||||
defaultExperimentState().compactMessageDisplay;
|
||||
if (
|
||||
typeof parsed?.compactMessageDisplay === 'string' &&
|
||||
['full', 'brief'].includes(parsed.compactMessageDisplay)
|
||||
) {
|
||||
compactMessageDisplay = parsed.compactMessageDisplay;
|
||||
}
|
||||
|
||||
return {
|
||||
blockDisplayMode
|
||||
blockDisplayMode,
|
||||
compactMessageDisplay
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('无法读取实验功能设置:', error);
|
||||
@ -760,6 +773,13 @@ export const usePersonalizationStore = defineStore('personalization', {
|
||||
};
|
||||
this.persistExperiments();
|
||||
},
|
||||
setCompactMessageDisplay(mode: CompactMessageDisplay) {
|
||||
this.experiments = {
|
||||
...this.experiments,
|
||||
compactMessageDisplay: mode
|
||||
};
|
||||
this.persistExperiments();
|
||||
},
|
||||
setCommunicationStyle(style: 'default' | 'human_like') {
|
||||
this.form = {
|
||||
...this.form,
|
||||
|
||||
@ -511,6 +511,38 @@
|
||||
border: 1px solid var(--theme-control-border);
|
||||
}
|
||||
|
||||
/* 简略信息:以横跨显示区域的横线包裹一行概要文字(类似 markdown 的 --- 分隔线)。 */
|
||||
.user-message.user-message--brief {
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-message.user-message--brief .compact-brief-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
width: min(760px, 92%);
|
||||
margin: 2px auto;
|
||||
color: var(--claude-text-secondary);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.user-message.user-message--brief .compact-brief-line::before,
|
||||
.user-message.user-message--brief .compact-brief-line::after {
|
||||
content: '';
|
||||
flex: 1 1 auto;
|
||||
height: 0;
|
||||
border-top: 1px solid var(--theme-control-border, var(--claude-border));
|
||||
}
|
||||
|
||||
.user-message.user-message--brief .compact-brief-text {
|
||||
flex: 0 0 auto;
|
||||
font-size: 12.5px;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.user-message .message-text .user-skill-link {
|
||||
color: #2563eb;
|
||||
font-weight: 600;
|
||||
|
||||
@ -6,6 +6,7 @@ const COMPACT_FALLBACK_SOURCES = new Set([
|
||||
'guidance',
|
||||
'goal_review',
|
||||
'compression',
|
||||
'compression_handoff',
|
||||
'sub_agent',
|
||||
'background_command'
|
||||
]);
|
||||
@ -72,7 +73,7 @@ export function messageStartsWork(message: any): boolean {
|
||||
if (source === 'guidance' || source === 'goal_prompt' || source === 'skill') {
|
||||
return false;
|
||||
}
|
||||
if (source === 'goal_review' || source === 'compression' || isLegacyGoalReview(message)) {
|
||||
if (source === 'goal_review' || source === 'compression' || source === 'compression_handoff' || isLegacyGoalReview(message)) {
|
||||
return true;
|
||||
}
|
||||
return LEGACY_STARTS_WORK_SOURCES.has(source || 'user');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user