fix: remove processing indicators, unblock input during agent activity

This commit is contained in:
2026-02-24 11:24:28 -06:00
parent 6fabf37196
commit c8b484b10e
2 changed files with 20 additions and 59 deletions

View File

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

View File

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