|
114 | 114 | ref="scrollEl" |
115 | 115 | class="flex-1 overflow-y-auto p-2 font-mono text-xs/snug" |
116 | 116 | @scroll="onScroll" |
| 117 | + @mousedown="onScrollMouseDown" |
117 | 118 | > |
118 | 119 | <div v-for="m in store.messages" :key="m.id" class="agent-block"> |
119 | 120 | <div |
|
204 | 205 | </div> |
205 | 206 | <div |
206 | 207 | v-if="store.messages.length === 0" |
207 | | - class="p-2 text-muted-foreground/70" |
| 208 | + class="text-muted-foreground/70" |
208 | 209 | > |
209 | 210 | {{ t('agent.panel.prompt') }} {{ t('agent.panel.brandTitle') }} |
210 | 211 | {{ t('agent.panel.readyHint') }} |
211 | 212 | </div> |
212 | | - </div> |
213 | 213 |
|
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 | | - > |
218 | 214 | <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" |
222 | 217 | > |
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" |
235 | 222 | > |
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> |
238 | 239 | </div> |
239 | | - </div> |
240 | 240 |
|
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> |
268 | 272 | </div> |
269 | 273 |
|
270 | 274 | <div |
@@ -617,6 +621,19 @@ function focusInput(): void { |
617 | 621 | void nextTick(() => inputEl.value?.focus()) |
618 | 622 | } |
619 | 623 |
|
| 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 | +
|
620 | 637 | async function onDrop(e: DragEvent): Promise<void> { |
621 | 638 | isHovering.value = false |
622 | 639 | const dt = e.dataTransfer |
|
0 commit comments