@@ -463,6 +498,7 @@ import { TextSelection } from 'prosemirror-state';
import QuickMenu from '@/components/input/QuickMenu.vue';
import FileAtMenu, { type FileAtItem } from '@/components/input/FileAtMenu.vue';
import RollingNumber from '@/components/input/RollingNumber.vue';
+import StatusAvatar from '@/components/avatar/StatusAvatar.vue';
import { useInputStore } from '@/stores/input';
import { usePersonalizationStore } from '@/stores/personalization';
@@ -579,6 +615,13 @@ const props = defineProps<{
hasPendingRuntimeGuidance?: boolean;
terminalCount?: number;
hostMode?: boolean;
+ avatarStatus?: {
+ mode: string;
+ toolKeys?: string[];
+ toolTexts?: string[];
+ text: string;
+ tracking?: boolean;
+ } | null;
}>();
const inputStore = useInputStore();
@@ -638,6 +681,31 @@ const fileAtMenuStyle = ref>({});
const fileAtPickerActive = ref(false);
let fileAtSearchTimer: number | null = null;
+const avatarPokeText = ref('');
+let avatarPokeTimer: number | null = null;
+const STATUS_AVATAR_POKE_TEXTS = [
+ '不要!',
+ '停下来!',
+ '别戳啦!',
+ '哎呀!',
+ '你戳到我了!',
+ '轻一点嘛!',
+ '我不是按钮!',
+ '不许乱点!',
+ '呜……别戳我了……',
+ '六边形也是会痛的!',
+ '再戳我要变形了!',
+ '我会紧张的……',
+ '正在假装镇定……',
+ 'Token 掉了一地!',
+ '上下文被你戳乱了!',
+ '防戳模式启动失败',
+ '系统提示:AI 被戳了一下',
+ '警告:检测到手欠行为',
+ '我记住你了!',
+ '戳回去!'
+];
+
const slashHighlightStyle = computed(() => {
if (slashManualScrolled.value) return { display: 'none' };
const isFirst = skillSlashActiveIndex.value === 0 && skillSlashList.value?.scrollTop === 0;
@@ -764,6 +832,22 @@ const floatingStatusVisible = computed(() => {
);
});
+const showComposerAvatar = computed(() => {
+ if (skillSlashMenuOpen.value || fileAtOpen.value || props.quickMenuOpen) return false;
+ if (runtimeQueuedMessagesForRender.value.length > 0 || props.hasPendingRuntimeGuidance)
+ return false;
+ return !!props.avatarStatus;
+});
+
+const composerAvatarText = computed(() => {
+ if (!props.avatarStatus) return '';
+ // 头像旁边显示 tool 模式的 intent 文案以及 think 模式的提示
+ if (props.avatarStatus.mode === 'tool' || props.avatarStatus.mode === 'think') {
+ return props.avatarStatus.text || '';
+ }
+ return '';
+});
+
const projectGitMenuOpen = ref(null);
const closeProjectGitMenu = () => {
@@ -869,6 +953,25 @@ const closeFileAtMenu = () => {
}
};
+const clearAvatarPokeText = () => {
+ if (avatarPokeTimer !== null) {
+ window.clearTimeout(avatarPokeTimer);
+ avatarPokeTimer = null;
+ }
+ avatarPokeText.value = '';
+};
+
+const handleAvatarPoke = () => {
+ if (!props.avatarStatus || props.avatarStatus.mode !== 'idle') return;
+ const texts = STATUS_AVATAR_POKE_TEXTS;
+ avatarPokeText.value = texts[Math.floor(Math.random() * texts.length)] || '不要!';
+ if (avatarPokeTimer !== null) window.clearTimeout(avatarPokeTimer);
+ avatarPokeTimer = window.setTimeout(() => {
+ avatarPokeText.value = '';
+ avatarPokeTimer = null;
+ }, 1800);
+};
+
const updateFileAtMenuPosition = (tokenEnd: number) => {
nextTick(() => {
const instance = editor.value;
@@ -2312,6 +2415,7 @@ const hasRuntimeLayoutExpansion = computed(() => {
return (
hasQueue ||
floatingStatusVisible.value ||
+ showComposerAvatar.value ||
skillSlashOpen.value ||
fileAtOpen.value ||
hasImages ||
@@ -2380,13 +2484,14 @@ const collectComposerVisualHeight = () => {
// 收起态横幅是脱离流、浮在角上的小圆点,不应计入为聊天区预留的高度,
// 否则会把消息区可滚动范围顶高。故排除 .goal-mode-banner--collapsed。
const nodes = root.querySelectorAll(
- '.runtime-queue-list:not(.runtime-queue-list--empty), .floating-project-status'
+ '.runtime-queue-list:not(.runtime-queue-list--empty), .floating-project-status, .composer-avatar'
);
nodes.forEach((node) => {
if (!(node instanceof HTMLElement)) return;
- // 离开态:floatingStatusVisible 已翻 false 但元素仍在 DOM 中做退场动画。
- // 此时不再计入它的高度,让输入栏立即回升、与状态栏的退场动画同步。
+ // 离开态:floatingStatusVisible / showComposerAvatar 已翻 false 但元素仍在 DOM 中做退场动画。
+ // 此时不再计入它的高度,让输入栏立即回升、与浮层的退场动画同步。
if (node.classList.contains('floating-project-status') && !floatingStatusVisible.value) return;
+ if (node.classList.contains('composer-avatar') && !showComposerAvatar.value) return;
top = Math.min(top, node.offsetTop);
bottom = Math.max(bottom, node.offsetTop + node.offsetHeight);
});
@@ -2418,7 +2523,7 @@ const emitComposerHeight = () => {
}
const baseline = baselineComposerVisualHeight.value || visualHeight;
const growth = Math.max(0, visualHeight - baseline);
- const baseReservedHeight = 93;
+ const baseReservedHeight = 123;
const reservedHeight = Math.ceil(baseReservedHeight + growth);
emit('composer-height-change', {
reservedHeight,
@@ -2755,6 +2860,21 @@ watch(floatingStatusVisible, async () => {
}, 280);
});
+watch(showComposerAvatar, async () => {
+ await nextTick();
+ emitComposerHeight();
+ window.setTimeout(() => {
+ emitComposerHeight();
+ }, 280);
+});
+
+watch(
+ () => props.avatarStatus?.mode,
+ () => {
+ if (props.avatarStatus?.mode !== 'idle') clearAvatarPokeText();
+ }
+);
+
watch(
() => [props.selectedImages?.length || 0, props.selectedVideos?.length || 0],
async () => {
@@ -2831,6 +2951,7 @@ onBeforeUnmount(() => {
composerResizeObserver.disconnect();
composerResizeObserver = null;
}
+ clearAvatarPokeText();
});
@@ -2855,6 +2976,61 @@ onBeforeUnmount(() => {
pointer-events: auto;
}
+.composer-avatar {
+ position: absolute;
+ left: 0;
+ bottom: calc(100% + 6px);
+ z-index: auto;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ height: 40px;
+ pointer-events: auto;
+ color: var(--claude-text);
+}
+
+.composer-avatar .status-avatar {
+ display: block;
+ flex-shrink: 0;
+}
+
+.composer-avatar__text {
+ position: absolute;
+ white-space: nowrap;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--claude-text-secondary);
+ pointer-events: none;
+ z-index: 1;
+}
+
+.composer-avatar__text--right {
+ left: calc(100% + 8px);
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.composer-avatar__text--top {
+ left: 0;
+ bottom: calc(100% + 4px);
+ transform: none;
+}
+
+.composer-avatar-text-fade-enter-active,
+.composer-avatar-text-fade-leave-active {
+ transition: opacity 0.18s ease;
+}
+
+.composer-avatar-text-fade-enter-from,
+.composer-avatar-text-fade-leave-to {
+ opacity: 0;
+}
+
+.composer-avatar-text-fade-enter-to,
+.composer-avatar-text-fade-leave-from {
+ opacity: 1;
+}
+
.floating-project-status__left,
.floating-project-status__right {
min-width: 0;
diff --git a/static/src/styles/components/chat/_chat-area.scss b/static/src/styles/components/chat/_chat-area.scss
index 9d185d5..ecfec95 100644
--- a/static/src/styles/components/chat/_chat-area.scss
+++ b/static/src/styles/components/chat/_chat-area.scss
@@ -126,76 +126,6 @@
margin-bottom: 0;
}
-.conversation-status-avatar {
- display: flex;
- align-items: center;
- gap: 10px;
- min-height: 40px;
- margin: 12px 0 18px;
- color: var(--text-secondary);
-}
-
-.conversation-status-avatar .status-avatar {
- color: var(--text-primary);
-}
-
-.conversation-status-avatar__text {
- min-width: 0;
- max-width: min(560px, 86%);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: var(--text-secondary);
- font-size: 13px;
- font-weight: 500;
- letter-spacing: 0.01em;
-}
-
-.conversation-status-avatar-text-fade-enter-active,
-.conversation-status-avatar-text-fade-leave-active {
- transition: opacity 0.18s ease;
-}
-
-.conversation-status-avatar-text-fade-enter-from,
-.conversation-status-avatar-text-fade-leave-to {
- opacity: 0;
-}
-
-.conversation-status-avatar-text-fade-enter-to,
-.conversation-status-avatar-text-fade-leave-from {
- opacity: 1;
-}
-
-.conversation-status-avatar__text--animated {
- display: inline-flex;
- align-items: center;
- gap: 0.02em;
- letter-spacing: 0.05em;
-}
-
-.conversation-status-avatar__letter {
- display: inline-block;
- opacity: 0.42;
- transform: translateY(0);
- animation: conversation-status-letter-wave 1.6s ease-in-out infinite;
-}
-
-@keyframes conversation-status-letter-wave {
- 0%,
- 100% {
- opacity: 0.42;
- transform: translateY(0);
- }
- 20% {
- opacity: 1;
- transform: translateY(-3px);
- }
- 42% {
- opacity: 0.68;
- transform: translateY(0);
- }
-}
-
.messages-bottom-spacer {
width: 1px;
height: var(--composer-growth-height, 0px);
@@ -611,6 +541,38 @@
color: var(--claude-text-secondary);
}
+.assistant-generating-placeholder {
+ display: inline-flex;
+ align-items: center;
+ color: var(--claude-text-secondary);
+ font-size: 14px;
+ font-weight: 500;
+ letter-spacing: 0.05em;
+}
+
+.assistant-generating-letter {
+ display: inline-block;
+ opacity: 0.42;
+ transform: translateY(0);
+ animation: assistant-generating-letter-wave 1.6s ease-in-out infinite;
+}
+
+@keyframes assistant-generating-letter-wave {
+ 0%,
+ 100% {
+ opacity: 0.42;
+ transform: translateY(0);
+ }
+ 20% {
+ opacity: 1;
+ transform: translateY(-3px);
+ }
+ 42% {
+ opacity: 0.68;
+ transform: translateY(0);
+ }
+}
+
.user-message .message-text,
.assistant-message .message-text {
padding: 16px 20px;