diff --git a/static/src/components/input/InputComposer.vue b/static/src/components/input/InputComposer.vue index 58adda1b..4dc5c574 100644 --- a/static/src/components/input/InputComposer.vue +++ b/static/src/components/input/InputComposer.vue @@ -8,8 +8,10 @@ :class="{ 'goal-mode-banner--running': goalRunning, 'goal-mode-banner--done': goalCompleted && !goalRunning, - 'goal-mode-banner--clickable': goalRunning || goalCompleted + 'goal-mode-banner--clickable': goalRunning || goalCompleted, + 'goal-mode-banner--collapsed': goalBannerCollapsed }" + :title="goalBannerCollapsed ? goalBannerTitle : undefined" @click.stop="goalRunning || goalCompleted ? $emit('open-goal-dialog') : null" > @@ -1040,6 +1042,18 @@ const hasRuntimeLayoutExpansion = computed(() => { const goalCompleted = computed(() => String(props.goalProgress?.status || '').toLowerCase() === 'done'); +// 当消息队列非空或 skill 选择菜单展开时,把目标横幅收起为仅圆点, +// 让队列/菜单紧贴输入框,不再被横幅顶高。队列清空且无 skill 选择时自动恢复。 +const goalBannerCollapsed = computed( + () => runtimeQueuedMessagesForRender.value.length > 0 || skillSlashMenuOpen.value +); + +const goalBannerTitle = computed(() => { + if (props.goalRunning) return '目标模式运行中'; + if (goalCompleted.value) return '目标模式完成'; + return '目标模式已就绪'; +}); + const collectComposerVisualHeight = () => { const root = inputAreaRoot.value; const shell = compactInputShell.value; @@ -1049,7 +1063,9 @@ const collectComposerVisualHeight = () => { const shellRect = shell.getBoundingClientRect(); let top = shellRect.top; let bottom = shellRect.bottom; - const nodes = root.querySelectorAll('.runtime-queue-list:not(.runtime-queue-list--empty), .skill-slash-menu, .goal-mode-banner'); + // 收起态横幅是脱离流、浮在角上的小圆点,不应计入为聊天区预留的高度, + // 否则会把消息区可滚动范围顶高。故排除 .goal-mode-banner--collapsed。 + const nodes = root.querySelectorAll('.runtime-queue-list:not(.runtime-queue-list--empty), .skill-slash-menu, .goal-mode-banner:not(.goal-mode-banner--collapsed)'); nodes.forEach((node) => { if (!(node instanceof HTMLElement)) return; const rect = node.getBoundingClientRect(); @@ -1258,6 +1274,13 @@ watch( } ); +// skill 菜单开关会切换目标横幅收起/展开态,需重新上报高度, +// 否则展开回来时聊天区预留高度不会同步恢复。 +watch(goalBannerCollapsed, async () => { + await nextTick(); + emitComposerHeight(); +}); + onMounted(() => { if (hasSkillMarkdownLink.value) { renderRichEditor(); @@ -1321,6 +1344,30 @@ onBeforeUnmount(() => { background: color-mix(in srgb, var(--theme-surface-soft, #fffaf0) 92%, var(--claude-text-tertiary, #a59a86) 8%); border: 1px solid var(--theme-chip-border, rgba(118, 103, 84, 0.2)); user-select: none; + transition: gap 0.2s ease, padding 0.2s ease; +} +/* 收起态:仅保留圆点与外圈,文字消失、右侧收拢,圆点位置(左侧 padding)不变。 + 关键:切到 absolute 脱离文档流,避免横幅占用一行流高度把队列/skill 菜单顶起来; + 锚定在输入框上方 6px、左 4px,与展开时的视觉位置一致,圆点位置不变。 */ +.goal-mode-banner--collapsed { + position: absolute; + left: 4px; + bottom: calc(100% + 6px); + z-index: 2; + margin: 0; + gap: 0; + padding-right: 10px; +} +.goal-mode-banner__text { + overflow: hidden; + white-space: nowrap; + max-width: 200px; + opacity: 1; + transition: max-width 0.2s ease, opacity 0.18s ease; +} +.goal-mode-banner--collapsed .goal-mode-banner__text { + max-width: 0; + opacity: 0; } .goal-mode-banner--running { color: var(--claude-accent, #da7756);