Skip to content

Commit beea15e

Browse files
committed
feat(agent): inline prompt — input flows as next scrollback line
Previously the textarea was a separate footer with its own border/bg, breaking the terminal illusion. Move the prompt+input INTO the scrollable container so it visually flows as the next line of scrollback — same font, same color scheme as the user-message blocks, no border, no background. - Pending-asset chips now render inline just above the prompt within the same scroll container (drops the heavy footer chrome). - Click any blank space in the scrollback focuses the input — like a real terminal where typing always lands at the prompt regardless of scroll position. Clicks on actual messages are left alone so text selection still works. - The prompt scrolls with content; auto-scroll-to-bottom keeps it in view when new messages arrive.
1 parent 03663fd commit beea15e

1 file changed

Lines changed: 68 additions & 51 deletions

File tree

src/agent/ui/FoldablePanel.vue

Lines changed: 68 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
ref="scrollEl"
115115
class="flex-1 overflow-y-auto p-2 font-mono text-xs/snug"
116116
@scroll="onScroll"
117+
@mousedown="onScrollMouseDown"
117118
>
118119
<div v-for="m in store.messages" :key="m.id" class="agent-block">
119120
<div
@@ -204,67 +205,70 @@
204205
</div>
205206
<div
206207
v-if="store.messages.length === 0"
207-
class="p-2 text-muted-foreground/70"
208+
class="text-muted-foreground/70"
208209
>
209210
{{ t('agent.panel.prompt') }} {{ t('agent.panel.brandTitle') }}
210211
{{ t('agent.panel.readyHint') }}
211212
</div>
212-
</div>
213213

214-
<div
215-
v-if="store.pendingAssets.length > 0"
216-
class="border-default/30 flex flex-wrap gap-1 border-t bg-secondary-background/40 p-1.5"
217-
>
218214
<div
219-
v-for="asset in store.pendingAssets"
220-
:key="asset.id"
221-
class="group flex items-center gap-1 rounded-sm bg-secondary-background px-1.5 py-0.5 text-xs"
215+
v-if="store.pendingAssets.length > 0"
216+
class="my-1 flex flex-wrap gap-1"
222217
>
223-
<img
224-
v-if="asset.previewUrl"
225-
:src="asset.previewUrl"
226-
:alt="asset.name"
227-
class="size-5 rounded-sm object-cover"
228-
/>
229-
<i v-else class="icon-[lucide--file] size-3" />
230-
<span class="max-w-32 truncate font-mono">{{ asset.path }}</span>
231-
<button
232-
class="opacity-50 hover:opacity-100"
233-
:aria-label="t('agent.input.removeAsset')"
234-
@click="store.removePendingAsset(asset.id)"
218+
<div
219+
v-for="asset in store.pendingAssets"
220+
:key="asset.id"
221+
class="group flex items-center gap-1 rounded-sm bg-secondary-background/60 px-1.5 py-0.5 text-xs"
235222
>
236-
<i class="icon-[lucide--x] size-3" />
237-
</button>
223+
<img
224+
v-if="asset.previewUrl"
225+
:src="asset.previewUrl"
226+
:alt="asset.name"
227+
class="size-5 rounded-sm object-cover"
228+
/>
229+
<i v-else class="icon-[lucide--file] size-3" />
230+
<span class="max-w-32 truncate">{{ asset.path }}</span>
231+
<button
232+
class="opacity-50 hover:opacity-100"
233+
:aria-label="t('agent.input.removeAsset')"
234+
@click="store.removePendingAsset(asset.id)"
235+
>
236+
<i class="icon-[lucide--x] size-3" />
237+
</button>
238+
</div>
238239
</div>
239-
</div>
240240

241-
<div
242-
class="border-default/30 flex items-end gap-1.5 border-t bg-secondary-background/30 px-2 py-1.5"
243-
>
244-
<span class="pt-1 font-mono text-xs text-azure-400 select-none">{{
245-
t('agent.panel.prompt')
246-
}}</span>
247-
<textarea
248-
ref="inputEl"
249-
v-model="inputText"
250-
rows="1"
251-
autocomplete="off"
252-
spellcheck="false"
253-
:placeholder="
254-
store.isStreaming
255-
? t('agent.panel.streamingPlaceholder')
256-
: t('agent.panel.inputPlaceholder')
257-
"
258-
:class="
259-
cn(
260-
'flex-1 resize-none bg-transparent font-mono text-xs/snug',
261-
'text-(--fg-color) placeholder:text-muted-foreground/50',
262-
'focus:outline-none'
263-
)
264-
"
265-
@keydown="onInputKey"
266-
@input="autoGrow"
267-
/>
241+
<!--
242+
Inline prompt — visually flows as the next line of scrollback
243+
rather than a separate input widget. Same font / colour scheme
244+
as user-message blocks; no border, no background.
245+
-->
246+
<div class="agent-prompt-row flex items-start gap-1.5">
247+
<span class="text-azure-400 select-none">{{
248+
t('agent.panel.prompt')
249+
}}</span>
250+
<textarea
251+
ref="inputEl"
252+
v-model="inputText"
253+
rows="1"
254+
autocomplete="off"
255+
spellcheck="false"
256+
:placeholder="
257+
store.isStreaming
258+
? t('agent.panel.streamingPlaceholder')
259+
: t('agent.panel.inputPlaceholder')
260+
"
261+
:class="
262+
cn(
263+
'flex-1 resize-none border-0 bg-transparent p-0 font-mono text-xs/snug',
264+
'text-(--fg-color) placeholder:text-muted-foreground/50',
265+
'focus:ring-0 focus:outline-none'
266+
)
267+
"
268+
@keydown="onInputKey"
269+
@input="autoGrow"
270+
/>
271+
</div>
268272
</div>
269273

270274
<div
@@ -617,6 +621,19 @@ function focusInput(): void {
617621
void nextTick(() => inputEl.value?.focus())
618622
}
619623
624+
/**
625+
* Click anywhere in the scrollback — but only on the bare container
626+
* itself, not on a message — focuses the input. Mirrors how a real
627+
* terminal lets you keep typing after scrolling away.
628+
*/
629+
function onScrollMouseDown(e: MouseEvent): void {
630+
const target = e.target as HTMLElement | null
631+
if (!target) return
632+
if (target === scrollEl.value) {
633+
focusInput()
634+
}
635+
}
636+
620637
async function onDrop(e: DragEvent): Promise<void> {
621638
isHovering.value = false
622639
const dt = e.dataTransfer

0 commit comments

Comments
 (0)