fix: Per-agent terminal isolation, floating terminal z-index, and char-by-char input
- Add :key to PromptBar to force remount on agent switch, fixing shared terminal session bug - Raise AgentTerminal z-index above PromptBar backdrop so floating terminal is visible/clickable - Send prompt text char-by-char (15ms delay) matching FloatingVoice pattern for Claude Code compat - Guard xterm dispose against unloaded addons to prevent errors on agent switch - Widen PromptBar panel from 360px to 420px to fit all ChatInput buttons
This commit is contained in:
@@ -3,11 +3,13 @@ import { ref, reactive, computed, watch, nextTick, onMounted, onBeforeUnmount }
|
||||
import type { Agent } from '../../types/agent'
|
||||
import { endpoints } from '../../config/endpoints'
|
||||
import { useVoiceCapture } from '../../composables/useVoiceCapture'
|
||||
import { useAgentTerminal } from '../../composables/useAgentTerminal'
|
||||
import { useCanvasStore } from '../../stores/canvas'
|
||||
import ChatInput from './ChatInput.vue'
|
||||
import TranscriptCard from './TranscriptCard.vue'
|
||||
import InputSettings from './InputSettings.vue'
|
||||
import ConversationHistory from './ConversationHistory.vue'
|
||||
import AgentTerminal from './AgentTerminal.vue'
|
||||
|
||||
interface ClaudeUsage {
|
||||
subscription: { type: string; tier: string; label: string; multiplier: number }
|
||||
@@ -23,6 +25,7 @@ interface ChatMessage {
|
||||
content: string
|
||||
status: 'sent' | 'thinking' | 'done'
|
||||
uuid?: string
|
||||
toolCalls?: string[]
|
||||
intervention?: {
|
||||
type: 'permission' | 'question' | 'plan'
|
||||
requestId?: string
|
||||
@@ -60,6 +63,32 @@ const voice = useVoiceCapture({
|
||||
onNotification: (msg, type, duration) => canvasStore.showNotification(msg, type, duration)
|
||||
})
|
||||
|
||||
// Agent terminal composable
|
||||
const agentTerminal = useAgentTerminal(props.agent.id)
|
||||
const showTerminal = ref(false)
|
||||
|
||||
const statusDot = computed<'green' | 'yellow' | 'red' | 'gray' | ''>(() => {
|
||||
switch (agentTerminal.terminalState.value) {
|
||||
case 'ready': return 'green'
|
||||
case 'connecting':
|
||||
case 'agent-starting': return 'yellow'
|
||||
case 'crashed': return 'red'
|
||||
case 'off': return 'gray'
|
||||
default: return ''
|
||||
}
|
||||
})
|
||||
|
||||
const inputPlaceholder = computed(() => {
|
||||
const label = props.agent.uiConfig?.label || props.agent.name
|
||||
switch (agentTerminal.terminalState.value) {
|
||||
case 'off': return `Agent offline - type to start ${label}`
|
||||
case 'crashed': return `Agent crashed - type to restart ${label}`
|
||||
case 'connecting':
|
||||
case 'agent-starting': return `Starting ${label}...`
|
||||
default: return `Mensaje a ${label}...`
|
||||
}
|
||||
})
|
||||
|
||||
const contentEl = ref<HTMLDivElement | null>(null)
|
||||
const chatInputEl = ref<InstanceType<typeof ChatInput> | null>(null)
|
||||
const isRecording = ref(false)
|
||||
@@ -96,7 +125,7 @@ const panelStyle = computed(() => {
|
||||
const bubbleCenterX = props.anchorRect.left + props.anchorRect.width / 2
|
||||
const bottomOffset = window.innerHeight - props.anchorRect.top + 12
|
||||
|
||||
const panelWidth = 360
|
||||
const panelWidth = 420
|
||||
let left = bubbleCenterX - panelWidth / 2
|
||||
left = Math.max(12, Math.min(left, window.innerWidth - panelWidth - 12))
|
||||
|
||||
@@ -112,6 +141,95 @@ const hasContent = computed(() =>
|
||||
messages.length > 0 || showTranscript.value || showHistory.value || showSettings.value
|
||||
)
|
||||
|
||||
// ── Tool chip registry ──
|
||||
|
||||
interface ToolMeta { icon: string; color: string; label?: string }
|
||||
|
||||
const TOOL_CATEGORIES: Record<string, ToolMeta> = {
|
||||
// File ops — cyan
|
||||
Read: { icon: '◉', color: '#22d3ee' },
|
||||
Edit: { icon: '✎', color: '#22d3ee' },
|
||||
Write: { icon: '✦', color: '#22d3ee' },
|
||||
Glob: { icon: '⊞', color: '#22d3ee' },
|
||||
Grep: { icon: '⊘', color: '#22d3ee' },
|
||||
// Terminal — green
|
||||
Bash: { icon: '▸', color: '#34d399' },
|
||||
KillShell: { icon: '✕', color: '#34d399', label: 'Kill' },
|
||||
// Tasks — amber
|
||||
Task: { icon: '◈', color: '#fbbf24' },
|
||||
TaskCreate: { icon: '+', color: '#fbbf24', label: 'Task+' },
|
||||
TaskUpdate: { icon: '↻', color: '#fbbf24', label: 'Task↻' },
|
||||
TaskList: { icon: '≡', color: '#fbbf24', label: 'Tasks' },
|
||||
TaskOutput: { icon: '◎', color: '#fbbf24', label: 'TaskOut' },
|
||||
TodoWrite: { icon: '☑', color: '#fbbf24', label: 'Todo' },
|
||||
// Web — purple
|
||||
WebSearch: { icon: '⌕', color: '#a78bfa' },
|
||||
WebFetch: { icon: '↓', color: '#a78bfa' },
|
||||
// Interaction — rose
|
||||
AskUserQuestion: { icon: '?', color: '#fb7185', label: 'Ask' },
|
||||
EnterPlanMode: { icon: '▤', color: '#fb7185', label: 'Plan↵' },
|
||||
ExitPlanMode: { icon: '▤', color: '#fb7185', label: 'Plan✓' },
|
||||
Skill: { icon: '⚡', color: '#fb7185' },
|
||||
// Resources — slate
|
||||
ListMcpResourcesTool: { icon: '☰', color: '#94a3b8', label: 'Resources' },
|
||||
}
|
||||
|
||||
function getToolMeta(name: string): ToolMeta {
|
||||
if (TOOL_CATEGORIES[name]) return TOOL_CATEGORIES[name]
|
||||
if (name.startsWith('mcp__')) {
|
||||
const short = name.split('-').pop() || name
|
||||
return { icon: '◆', color: '#818cf8', label: short }
|
||||
}
|
||||
return { icon: '•', color: '#94a3b8', label: name }
|
||||
}
|
||||
|
||||
// ── Display items with tool grouping ──
|
||||
|
||||
interface DisplayItem {
|
||||
type: 'message' | 'tool-group'
|
||||
msg?: ChatMessage
|
||||
tools?: { name: string; count: number; meta: ToolMeta }[]
|
||||
ids?: number[]
|
||||
}
|
||||
|
||||
function buildToolGroup(tools: string[], ids: number[]): DisplayItem {
|
||||
const counts = new Map<string, number>()
|
||||
for (const t of tools) counts.set(t, (counts.get(t) || 0) + 1)
|
||||
return {
|
||||
type: 'tool-group',
|
||||
tools: [...counts.entries()].map(([name, count]) => ({
|
||||
name, count, meta: getToolMeta(name)
|
||||
})),
|
||||
ids
|
||||
}
|
||||
}
|
||||
|
||||
const displayMessages = computed<DisplayItem[]>(() => {
|
||||
const items: DisplayItem[] = []
|
||||
let pendingTools: string[] = []
|
||||
let pendingIds: number[] = []
|
||||
|
||||
for (const msg of messages) {
|
||||
const isToolOnly = msg.toolCalls?.length && msg.content.startsWith('[Tool calls:')
|
||||
|
||||
if (isToolOnly) {
|
||||
pendingTools.push(...msg.toolCalls!)
|
||||
pendingIds.push(msg.id)
|
||||
} else {
|
||||
if (pendingTools.length) {
|
||||
items.push(buildToolGroup(pendingTools, pendingIds))
|
||||
pendingTools = []
|
||||
pendingIds = []
|
||||
}
|
||||
items.push({ type: 'message', msg })
|
||||
}
|
||||
}
|
||||
if (pendingTools.length) {
|
||||
items.push(buildToolGroup(pendingTools, pendingIds))
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
// ── Formatting helpers ──
|
||||
|
||||
function shortModel(model: string): string {
|
||||
@@ -241,7 +359,8 @@ function appendTranscriptMessages(newMsgs: any[]) {
|
||||
role: msg.role === 'user' ? 'user' : 'agent',
|
||||
content: msg.content || '',
|
||||
status: 'done',
|
||||
uuid: msg.uuid
|
||||
uuid: msg.uuid,
|
||||
toolCalls: msg.toolCalls || undefined
|
||||
})
|
||||
}
|
||||
|
||||
@@ -383,7 +502,8 @@ async function loadHistory(sessionId?: string) {
|
||||
role: msg.role === 'user' ? 'user' : 'agent',
|
||||
content: msg.content || '',
|
||||
status: 'done',
|
||||
uuid: msg.uuid
|
||||
uuid: msg.uuid,
|
||||
toolCalls: msg.toolCalls || undefined
|
||||
})
|
||||
}
|
||||
|
||||
@@ -437,6 +557,8 @@ function handleSessionChange(sessionId: string) {
|
||||
function handleSubmit(text: string) {
|
||||
messages.push({ id: ++idCounter, role: 'user', content: text, status: 'sent' })
|
||||
emit('submit', text)
|
||||
// Send text as input to the agent's terminal PTY
|
||||
agentTerminal.sendPrompt(text)
|
||||
// Real response will arrive via transcript-update WebSocket
|
||||
}
|
||||
|
||||
@@ -455,6 +577,7 @@ function handleTranscriptDone(text: string) {
|
||||
|
||||
messages.push({ id: ++idCounter, role: 'user', content: text, status: 'sent' })
|
||||
emit('submit', text)
|
||||
agentTerminal.sendPrompt(text)
|
||||
// Real response will arrive via transcript-update WebSocket
|
||||
}
|
||||
|
||||
@@ -467,6 +590,10 @@ function toggleHistory() {
|
||||
if (showHistory.value) scrollToBottom()
|
||||
}
|
||||
|
||||
function toggleTerminal() {
|
||||
showTerminal.value = !showTerminal.value
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') emit('close')
|
||||
}
|
||||
@@ -477,6 +604,7 @@ watch(() => props.visible, async (v) => {
|
||||
showTranscript.value = false
|
||||
showHistory.value = false
|
||||
showSettings.value = false
|
||||
showTerminal.value = false
|
||||
messages.length = 0
|
||||
idCounter = 0
|
||||
sessionStats.value = null
|
||||
@@ -493,6 +621,10 @@ watch(() => props.visible, async (v) => {
|
||||
// Connect WebSocket for real-time updates
|
||||
connectWs()
|
||||
|
||||
// Connect agent terminal (lazy)
|
||||
agentTerminal.connect()
|
||||
agentTerminal.checkStatus()
|
||||
|
||||
await voice.init()
|
||||
await nextTick()
|
||||
if (props.startRecording) {
|
||||
@@ -502,6 +634,8 @@ watch(() => props.visible, async (v) => {
|
||||
}
|
||||
} else {
|
||||
disconnectWs()
|
||||
agentTerminal.disconnect()
|
||||
showTerminal.value = false
|
||||
if (voice.isRecording.value) {
|
||||
voice.stopRecording()
|
||||
}
|
||||
@@ -518,6 +652,7 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
disconnectWs()
|
||||
agentTerminal.dispose()
|
||||
voice.cleanup()
|
||||
})
|
||||
</script>
|
||||
@@ -592,86 +727,109 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- Conversation content area -->
|
||||
<div ref="contentEl" class="pb-content" :class="{ empty: !hasContent }">
|
||||
<div
|
||||
v-for="msg in messages"
|
||||
:key="msg.id"
|
||||
class="chat-msg"
|
||||
:class="msg.role"
|
||||
>
|
||||
<template v-if="msg.role === 'user'">
|
||||
<div class="msg-bubble user-bubble">{{ msg.content }}</div>
|
||||
</template>
|
||||
<template v-for="(item, idx) in displayMessages" :key="item.msg?.id || item.ids?.join('-') || idx">
|
||||
<!-- Tool group: chips compactos -->
|
||||
<div v-if="item.type === 'tool-group'" class="tool-chips-row">
|
||||
<span
|
||||
v-for="tool in item.tools"
|
||||
:key="tool.name"
|
||||
class="tool-chip"
|
||||
:style="{ '--chip-color': tool.meta.color }"
|
||||
>
|
||||
<span class="chip-icon">{{ tool.meta.icon }}</span>
|
||||
<span class="chip-label">{{ tool.meta.label || tool.name }}</span>
|
||||
<span v-if="tool.count > 1" class="chip-count">{{ tool.count }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Intervention card -->
|
||||
<template v-else-if="msg.intervention">
|
||||
<div class="intervention-card" :class="`intervention--${msg.intervention.type}`">
|
||||
<!-- Permission card -->
|
||||
<template v-if="msg.intervention.type === 'permission'">
|
||||
<div class="intv-header">
|
||||
<svg class="intv-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>
|
||||
<span class="intv-title">Permission: {{ msg.intervention.toolName }}</span>
|
||||
</div>
|
||||
<div class="intv-detail">{{ msg.content }}</div>
|
||||
<div v-if="!msg.intervention.resolved" class="intv-actions">
|
||||
<button class="intv-btn intv-btn--allow" @click="respondPermission(msg.id, msg.intervention.requestId!, 'allow')">Allow</button>
|
||||
<button class="intv-btn intv-btn--deny" @click="respondPermission(msg.id, msg.intervention.requestId!, 'deny')">Deny</button>
|
||||
</div>
|
||||
<div v-else class="intv-resolved" :class="msg.intervention.decision === 'allow' ? 'resolved--allow' : 'resolved--deny'">
|
||||
{{ msg.intervention.decision === 'allow' ? 'Allowed' : 'Denied' }}
|
||||
</div>
|
||||
</template>
|
||||
<!-- Regular message -->
|
||||
<div v-else class="chat-msg" :class="item.msg!.role">
|
||||
<template v-if="item.msg!.role === 'user'">
|
||||
<div class="msg-bubble user-bubble">{{ item.msg!.content }}</div>
|
||||
</template>
|
||||
|
||||
<!-- Question card (info only) -->
|
||||
<template v-else-if="msg.intervention.type === 'question'">
|
||||
<div class="intv-header">
|
||||
<svg class="intv-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>
|
||||
<span class="intv-title">Question</span>
|
||||
</div>
|
||||
<div class="intv-detail">{{ msg.content }}</div>
|
||||
<div v-if="msg.intervention.options?.length" class="intv-options">
|
||||
<div v-for="(opt, i) in msg.intervention.options" :key="i" class="intv-option">
|
||||
<span class="opt-label">{{ opt.label }}</span>
|
||||
<span v-if="opt.description" class="opt-desc">{{ opt.description }}</span>
|
||||
<!-- Intervention card -->
|
||||
<template v-else-if="item.msg!.intervention">
|
||||
<div class="intervention-card" :class="`intervention--${item.msg!.intervention.type}`">
|
||||
<!-- Permission card -->
|
||||
<template v-if="item.msg!.intervention.type === 'permission'">
|
||||
<div class="intv-header">
|
||||
<svg class="intv-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>
|
||||
<span class="intv-title">Permission: {{ item.msg!.intervention.toolName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="intv-hint">Respond in terminal</div>
|
||||
</template>
|
||||
<div class="intv-detail">{{ item.msg!.content }}</div>
|
||||
<div v-if="!item.msg!.intervention.resolved" class="intv-actions">
|
||||
<button class="intv-btn intv-btn--allow" @click="respondPermission(item.msg!.id, item.msg!.intervention.requestId!, 'allow')">Allow</button>
|
||||
<button class="intv-btn intv-btn--deny" @click="respondPermission(item.msg!.id, item.msg!.intervention.requestId!, 'deny')">Deny</button>
|
||||
</div>
|
||||
<div v-else class="intv-resolved" :class="item.msg!.intervention.decision === 'allow' ? 'resolved--allow' : 'resolved--deny'">
|
||||
{{ item.msg!.intervention.decision === 'allow' ? 'Allowed' : 'Denied' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Plan card (info only) -->
|
||||
<template v-else-if="msg.intervention.type === 'plan'">
|
||||
<div class="intv-header">
|
||||
<svg class="intv-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
</svg>
|
||||
<span class="intv-title">{{ msg.content }}</span>
|
||||
</div>
|
||||
<div class="intv-hint">Review in terminal</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Question card (info only) -->
|
||||
<template v-else-if="item.msg!.intervention.type === 'question'">
|
||||
<div class="intv-header">
|
||||
<svg class="intv-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>
|
||||
<span class="intv-title">Question</span>
|
||||
</div>
|
||||
<div class="intv-detail">{{ item.msg!.content }}</div>
|
||||
<div v-if="item.msg!.intervention.options?.length" class="intv-options">
|
||||
<div v-for="(opt, i) in item.msg!.intervention.options" :key="i" class="intv-option">
|
||||
<span class="opt-label">{{ opt.label }}</span>
|
||||
<span v-if="opt.description" class="opt-desc">{{ opt.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="intv-hint">Respond in terminal</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="msg-bubble agent-bubble">
|
||||
<div v-if="msg.status === 'thinking'" class="thinking-inline">
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
<!-- Plan card (info only) -->
|
||||
<template v-else-if="item.msg!.intervention.type === 'plan'">
|
||||
<div class="intv-header">
|
||||
<svg class="intv-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
</svg>
|
||||
<span class="intv-title">{{ item.msg!.content }}</span>
|
||||
</div>
|
||||
<div class="intv-hint">Review in terminal</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="agent-text fade-in">{{ msg.content }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="msg-bubble agent-bubble">
|
||||
<div v-if="item.msg!.status === 'thinking'" class="thinking-inline">
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="agent-text fade-in">{{ item.msg!.content }}</div>
|
||||
<!-- Inline tool chips for mixed messages (text + tools) -->
|
||||
<div v-if="item.msg!.toolCalls?.length && !item.msg!.content.startsWith('[Tool calls:')" class="inline-tools">
|
||||
<span
|
||||
v-for="t in item.msg!.toolCalls"
|
||||
:key="t"
|
||||
class="tool-chip-mini"
|
||||
:style="{ '--chip-color': getToolMeta(t).color }"
|
||||
>{{ getToolMeta(t).icon }} {{ getToolMeta(t).label || t }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<TranscriptCard v-if="showTranscript" :voice="voice" @done="handleTranscriptDone" />
|
||||
<InputSettings
|
||||
@@ -688,17 +846,27 @@ onBeforeUnmount(() => {
|
||||
<!-- Input -->
|
||||
<ChatInput
|
||||
ref="chatInputEl"
|
||||
:placeholder="`Mensaje a ${agent.uiConfig?.label || agent.name}...`"
|
||||
:placeholder="inputPlaceholder"
|
||||
:recording="isRecording"
|
||||
:history-active="showHistory"
|
||||
:settings-active="showSettings"
|
||||
:terminal-active="showTerminal"
|
||||
:status-dot="statusDot"
|
||||
:autofocus="visible"
|
||||
@submit="handleSubmit"
|
||||
@mic="handleMic"
|
||||
@toggle-history="toggleHistory"
|
||||
@toggle-settings="toggleSettings"
|
||||
@toggle-terminal="toggleTerminal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Agent terminal floating window (shares composable instance) -->
|
||||
<AgentTerminal
|
||||
v-model="showTerminal"
|
||||
:agent="agent"
|
||||
:agent-terminal="agentTerminal"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
@@ -1137,6 +1305,74 @@ onBeforeUnmount(() => {
|
||||
50% { border-color: rgba(239, 68, 68, 0.4); }
|
||||
}
|
||||
|
||||
/* Tool chips row — consecutive tool calls share horizontal space */
|
||||
.tool-chips-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
animation: msg-in 0.2s ease-out;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.tool-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 10px;
|
||||
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
||||
background: color-mix(in srgb, var(--chip-color) 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--chip-color) 20%, transparent);
|
||||
color: var(--chip-color);
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.tool-chip:hover {
|
||||
background: color-mix(in srgb, var(--chip-color) 22%, transparent);
|
||||
}
|
||||
|
||||
.chip-icon {
|
||||
font-size: 10px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.chip-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chip-count {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
background: color-mix(in srgb, var(--chip-color) 25%, transparent);
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
min-width: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Reduce gap between consecutive tool rows */
|
||||
.tool-chips-row + .tool-chips-row {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
/* Inline tool chips for mixed messages (text + tool calls) */
|
||||
.inline-tools {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tool-chip-mini {
|
||||
font-size: 9px;
|
||||
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
||||
color: var(--chip-color);
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 768px) {
|
||||
.prompt-bar-panel {
|
||||
|
||||
Reference in New Issue
Block a user