fix: remove processing indicators, unblock input during agent activity
This commit is contained in:
@@ -380,26 +380,34 @@ defineExpose({ selectMode, toggleSelectMode, allCollapsed, collapseAllExceptLast
|
|||||||
// These skip the bounce animation and get a smooth transition instead
|
// These skip the bounce animation and get a smooth transition instead
|
||||||
const resolvedUuids = ref(new Set<string>())
|
const resolvedUuids = ref(new Set<string>())
|
||||||
|
|
||||||
|
// Force scroll to the absolute bottom of the container
|
||||||
|
function scrollToBottom() {
|
||||||
|
if (!scrollContainer.value) return
|
||||||
|
const el = scrollContainer.value
|
||||||
|
el.scrollTop = el.scrollHeight
|
||||||
|
// Second pass: content-visibility: auto can cause reflow after initial paint,
|
||||||
|
// so re-scroll after the browser finishes layout
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
el.scrollTop = el.scrollHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Scroll to bottom when a new transcript is loaded
|
// Scroll to bottom when a new transcript is loaded
|
||||||
watch(
|
watch(
|
||||||
() => props.conversation.sessionId,
|
() => props.conversation.sessionId,
|
||||||
async () => {
|
async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (scrollContainer.value) {
|
scrollToBottom()
|
||||||
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Auto-scroll to bottom when messages change
|
// Auto-scroll to bottom when messages change (every WS update)
|
||||||
watch(
|
watch(
|
||||||
() => props.conversation.messages.length,
|
() => props.conversation.messages.length,
|
||||||
async () => {
|
async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (scrollContainer.value) {
|
scrollToBottom()
|
||||||
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -674,7 +682,6 @@ function formatDuration(start: string, end: string): string {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<UserInput
|
<UserInput
|
||||||
:processing="props.processing"
|
|
||||||
:terminal-ready="props.terminalReady"
|
:terminal-ready="props.terminalReady"
|
||||||
:voice-transcript="voiceTranscript"
|
:voice-transcript="voiceTranscript"
|
||||||
:is-recording="isRecording"
|
:is-recording="isRecording"
|
||||||
@@ -999,6 +1006,8 @@ function formatDuration(start: string, end: string): string {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-wrapper.selected {
|
.message-wrapper.selected {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { ref, computed, watch, nextTick } from 'vue'
|
|||||||
import VoiceMicButton from './VoiceMicButton.vue'
|
import VoiceMicButton from './VoiceMicButton.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
processing?: boolean
|
|
||||||
terminalReady?: boolean | null // null = no terminal, false = starting, true = ready
|
terminalReady?: boolean | null // null = no terminal, false = starting, true = ready
|
||||||
voiceTranscript?: string
|
voiceTranscript?: string
|
||||||
isRecording?: boolean
|
isRecording?: boolean
|
||||||
@@ -25,10 +24,9 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const input = ref('')
|
const input = ref('')
|
||||||
|
|
||||||
// terminalReady: null = no terminal (show "no terminal"), false = starting, true = ready
|
// terminalReady: null = no terminal, false = starting, true = ready
|
||||||
const terminalStarting = computed(() => props.terminalReady === false) // explicitly false, not null
|
|
||||||
const noTerminal = computed(() => props.terminalReady === null)
|
const noTerminal = computed(() => props.terminalReady === null)
|
||||||
const canSend = computed(() => props.terminalReady === true && !props.processing)
|
const canSend = computed(() => props.terminalReady === true)
|
||||||
const isDisabled = computed(() => !input.value.trim() || !canSend.value)
|
const isDisabled = computed(() => !input.value.trim() || !canSend.value)
|
||||||
|
|
||||||
function handleSend() {
|
function handleSend() {
|
||||||
@@ -66,24 +64,12 @@ watch(() => props.voiceTranscript, (newText) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="user-input">
|
<div class="user-input">
|
||||||
<div v-if="terminalStarting" class="processing-bar starting">
|
|
||||||
<span class="processing-dots">
|
|
||||||
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
|
|
||||||
</span>
|
|
||||||
<span>Starting terminal...</span>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="processing" class="processing-bar">
|
|
||||||
<span class="processing-dots">
|
|
||||||
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
|
|
||||||
</span>
|
|
||||||
<span>Agent is processing...</span>
|
|
||||||
</div>
|
|
||||||
<div class="input-container" :class="{ disabled: !canSend }">
|
<div class="input-container" :class="{ disabled: !canSend }">
|
||||||
<textarea
|
<textarea
|
||||||
v-model="input"
|
v-model="input"
|
||||||
class="input-field"
|
class="input-field"
|
||||||
:style="{ maxHeight: maxH }"
|
:style="{ maxHeight: maxH }"
|
||||||
:placeholder="terminalStarting ? 'Starting terminal...' : noTerminal ? 'No terminal — use + to create session' : processing ? 'Wait for agent to finish...' : 'Continue this conversation...'"
|
:placeholder="noTerminal ? 'No terminal — use + to create session' : 'Continue this conversation...'"
|
||||||
rows="1"
|
rows="1"
|
||||||
:disabled="!canSend"
|
:disabled="!canSend"
|
||||||
@keydown="handleKeydown"
|
@keydown="handleKeydown"
|
||||||
@@ -120,40 +106,6 @@ watch(() => props.voiceTranscript, (newText) => {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.processing-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
padding: 0 0.25rem 0.35rem;
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.processing-bar.starting {
|
|
||||||
color: var(--text-muted, #888);
|
|
||||||
}
|
|
||||||
|
|
||||||
.processing-dots {
|
|
||||||
display: flex;
|
|
||||||
gap: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.processing-dots .dot {
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--accent);
|
|
||||||
animation: pulse-dot 1.2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.processing-dots .dot:nth-child(2) { animation-delay: 0.15s; }
|
|
||||||
.processing-dots .dot:nth-child(3) { animation-delay: 0.3s; }
|
|
||||||
|
|
||||||
@keyframes pulse-dot {
|
|
||||||
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
|
|
||||||
40% { opacity: 1; transform: scale(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-container {
|
.input-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
|||||||
Reference in New Issue
Block a user