Add agent attachments with @agent mentions
This commit is contained in:
148
src/components/agent-picker.tsx
Normal file
148
src/components/agent-picker.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { Component, createSignal, createEffect, For, Show, onCleanup } from "solid-js"
|
||||||
|
import type { Agent } from "../types/session"
|
||||||
|
|
||||||
|
interface AgentPickerProps {
|
||||||
|
open: boolean
|
||||||
|
onSelect: (agentName: string) => void
|
||||||
|
onClose: () => void
|
||||||
|
agents: Agent[]
|
||||||
|
searchQuery: string
|
||||||
|
textareaRef?: HTMLTextAreaElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const AgentPicker: Component<AgentPickerProps> = (props) => {
|
||||||
|
const [filteredAgents, setFilteredAgents] = createSignal<Agent[]>([])
|
||||||
|
const [selectedIndex, setSelectedIndex] = createSignal(0)
|
||||||
|
|
||||||
|
let containerRef: HTMLDivElement | undefined
|
||||||
|
let scrollContainerRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!props.open) return
|
||||||
|
|
||||||
|
const query = props.searchQuery.toLowerCase()
|
||||||
|
const filtered = query
|
||||||
|
? props.agents.filter(
|
||||||
|
(agent) =>
|
||||||
|
agent.name.toLowerCase().includes(query) ||
|
||||||
|
(agent.description && agent.description.toLowerCase().includes(query)),
|
||||||
|
)
|
||||||
|
: props.agents
|
||||||
|
|
||||||
|
setFilteredAgents(filtered)
|
||||||
|
setSelectedIndex(0)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (scrollContainerRef) {
|
||||||
|
scrollContainerRef.scrollTop = 0
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
function scrollToSelected() {
|
||||||
|
setTimeout(() => {
|
||||||
|
const selectedElement = containerRef?.querySelector('[data-agent-selected="true"]')
|
||||||
|
if (selectedElement) {
|
||||||
|
selectedElement.scrollIntoView({ block: "nearest", behavior: "smooth" })
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(agentName: string) {
|
||||||
|
props.onSelect(agentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (!props.open) return
|
||||||
|
|
||||||
|
const agents = filteredAgents()
|
||||||
|
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault()
|
||||||
|
setSelectedIndex((prev) => Math.min(prev + 1, agents.length - 1))
|
||||||
|
scrollToSelected()
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault()
|
||||||
|
setSelectedIndex((prev) => Math.max(prev - 1, 0))
|
||||||
|
scrollToSelected()
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
e.preventDefault()
|
||||||
|
const selected = agents[selectedIndex()]
|
||||||
|
if (selected) {
|
||||||
|
handleSelect(selected.name)
|
||||||
|
}
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
e.preventDefault()
|
||||||
|
props.onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.open) {
|
||||||
|
document.addEventListener("keydown", handleKeyDown)
|
||||||
|
onCleanup(() => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={props.open}>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
class="absolute bottom-full left-0 mb-1 w-full max-w-md rounded-md border border-gray-300 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800 z-50"
|
||||||
|
>
|
||||||
|
<div class="border-b border-gray-200 px-3 py-2 dark:border-gray-700">
|
||||||
|
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">Select Agent</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={scrollContainerRef} class="max-h-60 overflow-y-auto">
|
||||||
|
<Show when={filteredAgents().length === 0}>
|
||||||
|
<div class="px-3 py-4 text-center text-sm text-gray-500 dark:text-gray-400">No agents found</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<For each={filteredAgents()}>
|
||||||
|
{(agent, index) => (
|
||||||
|
<div
|
||||||
|
class={`cursor-pointer px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 ${
|
||||||
|
index() === selectedIndex() ? "bg-blue-50 dark:bg-blue-900/20" : ""
|
||||||
|
}`}
|
||||||
|
data-agent-selected={index() === selectedIndex()}
|
||||||
|
onClick={() => handleSelect(agent.name)}
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">{agent.name}</span>
|
||||||
|
<Show when={agent.mode === "subagent"}>
|
||||||
|
<span class="rounded bg-blue-50 px-1.5 py-0.5 text-xs font-normal text-blue-600 dark:bg-blue-500/20 dark:text-blue-400">
|
||||||
|
subagent
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={agent.description}>
|
||||||
|
<div class="mt-0.5 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{agent.description && agent.description.length > 80
|
||||||
|
? agent.description.slice(0, 80) + "..."
|
||||||
|
: agent.description}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 px-3 py-2 dark:border-gray-700">
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<span class="font-medium">↑↓</span> navigate • <span class="font-medium">Enter</span> select •{" "}
|
||||||
|
<span class="font-medium">Esc</span> close
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AgentPicker
|
||||||
@@ -2,13 +2,15 @@ import { createSignal, Show, onMount, For, onCleanup } from "solid-js"
|
|||||||
import AgentSelector from "./agent-selector"
|
import AgentSelector from "./agent-selector"
|
||||||
import ModelSelector from "./model-selector"
|
import ModelSelector from "./model-selector"
|
||||||
import FilePicker from "./file-picker"
|
import FilePicker from "./file-picker"
|
||||||
|
import AgentPicker from "./agent-picker"
|
||||||
import { addToHistory, getHistory } from "../stores/message-history"
|
import { addToHistory, getHistory } from "../stores/message-history"
|
||||||
import { getAttachments, addAttachment, clearAttachments, removeAttachment } from "../stores/attachments"
|
import { getAttachments, addAttachment, clearAttachments, removeAttachment } from "../stores/attachments"
|
||||||
import { createFileAttachment, createTextAttachment } from "../types/attachment"
|
import { createFileAttachment, createTextAttachment, createAgentAttachment } from "../types/attachment"
|
||||||
import type { Attachment } from "../types/attachment"
|
import type { Attachment } from "../types/attachment"
|
||||||
import Kbd from "./kbd"
|
import Kbd from "./kbd"
|
||||||
import HintRow from "./hint-row"
|
import HintRow from "./hint-row"
|
||||||
import { getActiveInstance } from "../stores/instances"
|
import { getActiveInstance } from "../stores/instances"
|
||||||
|
import { agents } from "../stores/sessions"
|
||||||
|
|
||||||
interface PromptInputProps {
|
interface PromptInputProps {
|
||||||
instanceId: string
|
instanceId: string
|
||||||
@@ -29,7 +31,9 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
const [historyIndex, setHistoryIndex] = createSignal(-1)
|
const [historyIndex, setHistoryIndex] = createSignal(-1)
|
||||||
const [isFocused, setIsFocused] = createSignal(false)
|
const [isFocused, setIsFocused] = createSignal(false)
|
||||||
const [showFilePicker, setShowFilePicker] = createSignal(false)
|
const [showFilePicker, setShowFilePicker] = createSignal(false)
|
||||||
|
const [showAgentPicker, setShowAgentPicker] = createSignal(false)
|
||||||
const [fileSearchQuery, setFileSearchQuery] = createSignal("")
|
const [fileSearchQuery, setFileSearchQuery] = createSignal("")
|
||||||
|
const [agentSearchQuery, setAgentSearchQuery] = createSignal("")
|
||||||
const [atPosition, setAtPosition] = createSignal<number | null>(null)
|
const [atPosition, setAtPosition] = createSignal<number | null>(null)
|
||||||
const [isDragging, setIsDragging] = createSignal(false)
|
const [isDragging, setIsDragging] = createSignal(false)
|
||||||
const [ignoredAtPositions, setIgnoredAtPositions] = createSignal<Set<number>>(new Set())
|
const [ignoredAtPositions, setIgnoredAtPositions] = createSignal<Set<number>>(new Set())
|
||||||
@@ -38,6 +42,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
let containerRef: HTMLDivElement | undefined
|
let containerRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
const attachments = () => getAttachments(props.instanceId, props.sessionId)
|
const attachments = () => getAttachments(props.instanceId, props.sessionId)
|
||||||
|
const instanceAgents = () => agents().get(props.instanceId) || []
|
||||||
|
|
||||||
function handleRemoveAttachment(attachmentId: string) {
|
function handleRemoveAttachment(attachmentId: string) {
|
||||||
const currentAttachments = attachments()
|
const currentAttachments = attachments()
|
||||||
@@ -52,6 +57,9 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
if (attachment.source.type === "file") {
|
if (attachment.source.type === "file") {
|
||||||
const filename = attachment.filename
|
const filename = attachment.filename
|
||||||
newPrompt = currentPrompt.replace(`@${filename}`, "").replace(/\s+/g, " ").trim()
|
newPrompt = currentPrompt.replace(`@${filename}`, "").replace(/\s+/g, " ").trim()
|
||||||
|
} else if (attachment.source.type === "agent") {
|
||||||
|
const agentName = attachment.filename
|
||||||
|
newPrompt = currentPrompt.replace(`@${agentName}`, "").replace(/\s+/g, " ").trim()
|
||||||
} else if (attachment.source.type === "text") {
|
} else if (attachment.source.type === "text") {
|
||||||
const placeholderMatch = attachment.display.match(/pasted #(\d+)/)
|
const placeholderMatch = attachment.display.match(/pasted #(\d+)/)
|
||||||
if (placeholderMatch) {
|
if (placeholderMatch) {
|
||||||
@@ -198,13 +206,13 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileMentionRegex = /@(\S+)/g
|
const mentionRegex = /@(\S+)/g
|
||||||
let fileMatch
|
let mentionMatch
|
||||||
|
|
||||||
while ((fileMatch = fileMentionRegex.exec(text)) !== null) {
|
while ((mentionMatch = mentionRegex.exec(text)) !== null) {
|
||||||
const mentionStart = fileMatch.index
|
const mentionStart = mentionMatch.index
|
||||||
const mentionEnd = fileMatch.index + fileMatch[0].length
|
const mentionEnd = mentionMatch.index + mentionMatch[0].length
|
||||||
const filename = fileMatch[1]
|
const name = mentionMatch[1]
|
||||||
|
|
||||||
const isDeletingFromEnd = e.key === "Backspace" && cursorPos === mentionEnd
|
const isDeletingFromEnd = e.key === "Backspace" && cursorPos === mentionEnd
|
||||||
const isDeletingFromStart = e.key === "Delete" && cursorPos === mentionStart
|
const isDeletingFromStart = e.key === "Delete" && cursorPos === mentionStart
|
||||||
@@ -215,7 +223,9 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
|
|
||||||
if (isDeletingFromEnd || isDeletingFromStart || isSelected) {
|
if (isDeletingFromEnd || isDeletingFromStart || isSelected) {
|
||||||
const currentAttachments = attachments()
|
const currentAttachments = attachments()
|
||||||
const attachment = currentAttachments.find((a) => a.source.type === "file" && a.filename === filename)
|
const attachment = currentAttachments.find(
|
||||||
|
(a) => (a.source.type === "file" || a.source.type === "agent") && a.filename === name,
|
||||||
|
)
|
||||||
|
|
||||||
if (attachment) {
|
if (attachment) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -243,7 +253,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === "Enter" && !e.shiftKey && !showFilePicker()) {
|
if (e.key === "Enter" && !e.shiftKey && !showFilePicker() && !showAgentPicker()) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleSend()
|
handleSend()
|
||||||
return
|
return
|
||||||
@@ -252,7 +262,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
const atStart = textarea.selectionStart === 0 && textarea.selectionEnd === 0
|
const atStart = textarea.selectionStart === 0 && textarea.selectionEnd === 0
|
||||||
const currentHistory = history()
|
const currentHistory = history()
|
||||||
|
|
||||||
if (e.key === "ArrowUp" && !showFilePicker() && atStart && currentHistory.length > 0) {
|
if (e.key === "ArrowUp" && !showFilePicker() && !showAgentPicker() && atStart && currentHistory.length > 0) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const newIndex = historyIndex() === -1 ? 0 : Math.min(historyIndex() + 1, currentHistory.length - 1)
|
const newIndex = historyIndex() === -1 ? 0 : Math.min(historyIndex() + 1, currentHistory.length - 1)
|
||||||
setHistoryIndex(newIndex)
|
setHistoryIndex(newIndex)
|
||||||
@@ -264,7 +274,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === "ArrowDown" && !showFilePicker() && historyIndex() >= 0) {
|
if (e.key === "ArrowDown" && !showFilePicker() && !showAgentPicker() && historyIndex() >= 0) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const newIndex = historyIndex() - 1
|
const newIndex = historyIndex() - 1
|
||||||
if (newIndex >= 0) {
|
if (newIndex >= 0) {
|
||||||
@@ -345,14 +355,28 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
if (!hasSpace && cursorPos === lastAtIndex + textAfterAt.length + 1) {
|
if (!hasSpace && cursorPos === lastAtIndex + textAfterAt.length + 1) {
|
||||||
if (!ignoredAtPositions().has(lastAtIndex)) {
|
if (!ignoredAtPositions().has(lastAtIndex)) {
|
||||||
setAtPosition(lastAtIndex)
|
setAtPosition(lastAtIndex)
|
||||||
setFileSearchQuery(textAfterAt)
|
|
||||||
setShowFilePicker(true)
|
const availableAgents = instanceAgents()
|
||||||
|
const matchesAgent = availableAgents.some((agent) =>
|
||||||
|
agent.name.toLowerCase().includes(textAfterAt.toLowerCase()),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (matchesAgent && textAfterAt.length > 0) {
|
||||||
|
setAgentSearchQuery(textAfterAt)
|
||||||
|
setShowAgentPicker(true)
|
||||||
|
setShowFilePicker(false)
|
||||||
|
} else {
|
||||||
|
setFileSearchQuery(textAfterAt)
|
||||||
|
setShowFilePicker(true)
|
||||||
|
setShowAgentPicker(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowFilePicker(false)
|
setShowFilePicker(false)
|
||||||
|
setShowAgentPicker(false)
|
||||||
setAtPosition(null)
|
setAtPosition(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,6 +459,56 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
|
|
||||||
function handleFilePickerNavigate(_direction: "up" | "down") {}
|
function handleFilePickerNavigate(_direction: "up" | "down") {}
|
||||||
|
|
||||||
|
function handleAgentSelect(agentName: string) {
|
||||||
|
const existingAttachments = attachments()
|
||||||
|
const alreadyAttached = existingAttachments.some(
|
||||||
|
(att) => att.source.type === "agent" && att.source.name === agentName,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!alreadyAttached) {
|
||||||
|
const attachment = createAgentAttachment(agentName)
|
||||||
|
addAttachment(props.instanceId, props.sessionId, attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPrompt = prompt()
|
||||||
|
const pos = atPosition()
|
||||||
|
const cursorPos = textareaRef?.selectionStart || 0
|
||||||
|
|
||||||
|
if (pos !== null) {
|
||||||
|
const before = currentPrompt.substring(0, pos)
|
||||||
|
const after = currentPrompt.substring(cursorPos)
|
||||||
|
const attachmentText = `@${agentName}`
|
||||||
|
const newPrompt = before + attachmentText + " " + after
|
||||||
|
setPrompt(newPrompt)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (textareaRef) {
|
||||||
|
const newCursorPos = pos + attachmentText.length + 1
|
||||||
|
textareaRef.setSelectionRange(newCursorPos, newCursorPos)
|
||||||
|
textareaRef.style.height = "auto"
|
||||||
|
textareaRef.style.height = Math.min(textareaRef.scrollHeight, 200) + "px"
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowAgentPicker(false)
|
||||||
|
setAtPosition(null)
|
||||||
|
setAgentSearchQuery("")
|
||||||
|
|
||||||
|
textareaRef?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAgentPickerClose() {
|
||||||
|
const pos = atPosition()
|
||||||
|
if (pos !== null) {
|
||||||
|
setIgnoredAtPositions((prev) => new Set(prev).add(pos))
|
||||||
|
}
|
||||||
|
setShowAgentPicker(false)
|
||||||
|
setAtPosition(null)
|
||||||
|
setAgentSearchQuery("")
|
||||||
|
setTimeout(() => textareaRef?.focus(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
function handleDragOver(e: DragEvent) {
|
function handleDragOver(e: DragEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -494,6 +568,17 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={showAgentPicker()}>
|
||||||
|
<AgentPicker
|
||||||
|
open={showAgentPicker()}
|
||||||
|
onClose={handleAgentPickerClose}
|
||||||
|
onSelect={handleAgentSelect}
|
||||||
|
agents={instanceAgents()}
|
||||||
|
searchQuery={agentSearchQuery()}
|
||||||
|
textareaRef={textareaRef}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col">
|
<div class="flex flex-1 flex-col">
|
||||||
<Show when={attachments().length > 0}>
|
<Show when={attachments().length > 0}>
|
||||||
<div class="flex flex-wrap gap-1.5 border-b border-gray-200 pb-2 dark:border-gray-700">
|
<div class="flex flex-wrap gap-1.5 border-b border-gray-200 pb-2 dark:border-gray-700">
|
||||||
@@ -503,14 +588,28 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
<Show
|
<Show
|
||||||
when={attachment.source.type === "text"}
|
when={attachment.source.type === "text"}
|
||||||
fallback={
|
fallback={
|
||||||
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<Show
|
||||||
<path
|
when={attachment.source.type === "agent"}
|
||||||
stroke-linecap="round"
|
fallback={
|
||||||
stroke-linejoin="round"
|
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
stroke-width="2"
|
<path
|
||||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
stroke-linecap="round"
|
||||||
/>
|
stroke-linejoin="round"
|
||||||
</svg>
|
stroke-width="2"
|
||||||
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Show>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@@ -545,7 +644,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
class="prompt-input"
|
class="prompt-input"
|
||||||
placeholder="Type your message, @file, or /command..."
|
placeholder="Type your message, @file, @agent, or /command..."
|
||||||
value={prompt()}
|
value={prompt()}
|
||||||
onInput={handleInput}
|
onInput={handleInput}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
@@ -566,8 +665,8 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="prompt-input-hints">
|
<div class="prompt-input-hints">
|
||||||
<HintRow>
|
<HintRow>
|
||||||
<Kbd>Enter</Kbd> to send • <Kbd>Shift+Enter</Kbd> for new line • <Kbd>@</Kbd> for files • <Kbd>↑↓</Kbd> for
|
<Kbd>Enter</Kbd> to send • <Kbd>Shift+Enter</Kbd> for new line • <Kbd>@</Kbd> for files/agents • <Kbd>↑↓</Kbd>{" "}
|
||||||
history
|
for history
|
||||||
<Show when={attachments().length > 0}>
|
<Show when={attachments().length > 0}>
|
||||||
<span class="ml-2 text-xs text-gray-500">• {attachments().length} file(s) attached</span>
|
<span class="ml-2 text-xs text-gray-500">• {attachments().length} file(s) attached</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -93,3 +93,18 @@ export function createTextAttachment(value: string, display: string, filename: s
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createAgentAttachment(agentName: string): Attachment {
|
||||||
|
return {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: "agent",
|
||||||
|
display: `@${agentName}`,
|
||||||
|
url: "",
|
||||||
|
filename: agentName,
|
||||||
|
mediaType: "text/plain",
|
||||||
|
source: {
|
||||||
|
type: "agent",
|
||||||
|
name: agentName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user