feat: Add specialized tool cards for transcript-debug and glassmorphism bubbles
Add toolCards/ with rich visual cards for 10 tool types: - AskUserQuestion, ExitPlanMode, EnterPlanMode - Read, Write, Bash, Edit - Grep, Glob - Task/TaskCreate/TaskUpdate/TaskGet/TaskList (unified TaskCard) Add MarkdownContent component and markdown/syntax highlight utils. Make user/assistant bubbles transparent with backdrop blur.
This commit is contained in:
@@ -3,6 +3,19 @@ import { computed } from 'vue'
|
||||
import type { ParsedAssistantMessage } from '@/types/transcript-debug'
|
||||
import ThinkingBlock from './ThinkingBlock.vue'
|
||||
import ToolCallBlock from './ToolCallBlock.vue'
|
||||
import MarkdownContent from './MarkdownContent.vue'
|
||||
import AskUserQuestionCard from './toolCards/AskUserQuestionCard.vue'
|
||||
import ExitPlanModeCard from './toolCards/ExitPlanModeCard.vue'
|
||||
import EnterPlanModeCard from './toolCards/EnterPlanModeCard.vue'
|
||||
import ReadCard from './toolCards/ReadCard.vue'
|
||||
import WriteCard from './toolCards/WriteCard.vue'
|
||||
import BashCard from './toolCards/BashCard.vue'
|
||||
import EditCard from './toolCards/EditCard.vue'
|
||||
import GrepCard from './toolCards/GrepCard.vue'
|
||||
import GlobCard from './toolCards/GlobCard.vue'
|
||||
import TaskCard from './toolCards/TaskCard.vue'
|
||||
|
||||
const TASK_TOOLS = new Set(['Task', 'TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList'])
|
||||
|
||||
const props = defineProps<{
|
||||
message: ParsedAssistantMessage
|
||||
@@ -62,24 +75,36 @@ function formatTokens(n?: number): string {
|
||||
:content="t"
|
||||
/>
|
||||
|
||||
<!-- Text content (filtered) -->
|
||||
<div v-for="(text, i) in visibleTextBlocks" :key="'text-' + i" class="text-content">
|
||||
{{ text }}
|
||||
</div>
|
||||
<!-- Text content (filtered, markdown-rendered) -->
|
||||
<MarkdownContent
|
||||
v-for="(text, i) in visibleTextBlocks"
|
||||
:key="'text-' + i"
|
||||
:content="text"
|
||||
/>
|
||||
|
||||
<!-- Tool calls -->
|
||||
<ToolCallBlock
|
||||
v-for="tc in message.toolCalls"
|
||||
:key="tc.id"
|
||||
:call="tc"
|
||||
/>
|
||||
<template v-for="tc in message.toolCalls" :key="tc.id">
|
||||
<AskUserQuestionCard v-if="tc.name === 'AskUserQuestion'" :call="tc" />
|
||||
<ExitPlanModeCard v-else-if="tc.name === 'ExitPlanMode'" :call="tc" />
|
||||
<EnterPlanModeCard v-else-if="tc.name === 'EnterPlanMode'" :call="tc" />
|
||||
<ReadCard v-else-if="tc.name === 'Read'" :call="tc" />
|
||||
<WriteCard v-else-if="tc.name === 'Write'" :call="tc" />
|
||||
<BashCard v-else-if="tc.name === 'Bash'" :call="tc" />
|
||||
<EditCard v-else-if="tc.name === 'Edit'" :call="tc" />
|
||||
<GrepCard v-else-if="tc.name === 'Grep'" :call="tc" />
|
||||
<GlobCard v-else-if="tc.name === 'Glob'" :call="tc" />
|
||||
<TaskCard v-else-if="TASK_TOOLS.has(tc.name)" :call="tc" />
|
||||
<ToolCallBlock v-else :call="tc" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.assistant-bubble {
|
||||
background: rgba(34, 197, 94, 0.05);
|
||||
border: 1px solid rgba(34, 197, 94, 0.15);
|
||||
background: rgba(34, 197, 94, 0.03);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
border: 1px solid rgba(34, 197, 94, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-right: 2rem;
|
||||
|
||||
25
frontend/src/components/transcript-debug/MarkdownContent.vue
Normal file
25
frontend/src/components/transcript-debug/MarkdownContent.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { parseMarkdown, ensureStyles } from '@/utils/markdown'
|
||||
|
||||
const props = defineProps<{
|
||||
content: string
|
||||
}>()
|
||||
|
||||
const html = computed(() => parseMarkdown(props.content))
|
||||
|
||||
onMounted(ensureStyles)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="md-content" v-html="html"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.md-content {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import MarkdownContent from './MarkdownContent.vue'
|
||||
|
||||
defineProps<{
|
||||
content: string
|
||||
@@ -21,7 +22,9 @@ const expanded = ref(false)
|
||||
<span class="thinking-label">Thinking</span>
|
||||
<span class="thinking-length">{{ content.length }} chars</span>
|
||||
</button>
|
||||
<div v-if="expanded" class="thinking-content">{{ content }}</div>
|
||||
<div v-if="expanded" class="thinking-content">
|
||||
<MarkdownContent :content="content" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import type { ParsedToolCall, ParsedProgressEvent } from '@/types/transcript-debug'
|
||||
import ToolResultBlock from './ToolResultBlock.vue'
|
||||
import { highlightCode } from '@/utils/markdown'
|
||||
|
||||
const props = defineProps<{
|
||||
call: ParsedToolCall
|
||||
@@ -28,6 +29,10 @@ function hookLabel(e: ParsedProgressEvent): string {
|
||||
const event = e.hookEvent || e.hookName.split(':')[0]
|
||||
return event
|
||||
}
|
||||
|
||||
const highlightedInput = computed(() =>
|
||||
highlightCode(JSON.stringify(props.call.input, null, 2), 'json')
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -101,7 +106,7 @@ function hookLabel(e: ParsedProgressEvent): string {
|
||||
</svg>
|
||||
<span>Input</span>
|
||||
</button>
|
||||
<pre v-if="inputExpanded" class="input-json">{{ JSON.stringify(call.input, null, 2) }}</pre>
|
||||
<pre v-if="inputExpanded" class="input-json" v-html="highlightedInput"></pre>
|
||||
</div>
|
||||
|
||||
<!-- Tool result -->
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { ParsedToolResult } from '@/types/transcript-debug'
|
||||
import { highlightCode } from '@/utils/markdown'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
result: ParsedToolResult
|
||||
}>()
|
||||
|
||||
const expanded = ref(false)
|
||||
|
||||
const highlightedContent = computed(() => {
|
||||
const content = props.result.content
|
||||
// Detect JSON
|
||||
const trimmed = content.trim()
|
||||
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
||||
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
||||
try {
|
||||
// Re-format + highlight
|
||||
const parsed = JSON.parse(trimmed)
|
||||
return highlightCode(JSON.stringify(parsed, null, 2), 'json')
|
||||
} catch { /* not valid JSON */ }
|
||||
}
|
||||
// Otherwise highlight as generic text
|
||||
return highlightCode(content)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -25,7 +42,7 @@ const expanded = ref(false)
|
||||
<span class="result-label">Result</span>
|
||||
<span class="result-size">{{ result.content.length }} chars</span>
|
||||
</button>
|
||||
<pre v-if="expanded" class="result-content">{{ result.content }}</pre>
|
||||
<pre v-if="expanded" class="result-content" v-html="highlightedContent"></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -33,8 +33,10 @@ function formatTime(ts: string): string {
|
||||
|
||||
<style scoped>
|
||||
.user-bubble {
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
background: rgba(99, 102, 241, 0.04);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
border: 1px solid rgba(99, 102, 241, 0.12);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-left: 2rem;
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { ParsedToolCall } from '@/types/transcript-debug'
|
||||
|
||||
const props = defineProps<{
|
||||
call: ParsedToolCall
|
||||
}>()
|
||||
|
||||
interface QuestionOption {
|
||||
label: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface QuestionItem {
|
||||
question: string
|
||||
header?: string
|
||||
options?: QuestionOption[]
|
||||
multiSelect?: boolean
|
||||
}
|
||||
|
||||
const questions = computed<QuestionItem[]>(() => {
|
||||
const qs = props.call.input?.questions
|
||||
if (Array.isArray(qs)) return qs as QuestionItem[]
|
||||
return []
|
||||
})
|
||||
|
||||
// Parse the answer from the result
|
||||
const answer = computed<Record<string, string>>(() => {
|
||||
if (!props.call.result?.content) return {}
|
||||
const raw = props.call.result.content
|
||||
|
||||
// Try to extract "answered: X" pattern from result text
|
||||
// Format: 'User has answered your questions: "question"="answer"'
|
||||
const answerMap: Record<string, string> = {}
|
||||
const regex = /"([^"]+)"="([^"]+)"/g
|
||||
let match
|
||||
while ((match = regex.exec(raw)) !== null) {
|
||||
answerMap[match[1]] = match[2]
|
||||
}
|
||||
|
||||
// Also try JSON format { answers: { ... } }
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (parsed?.answers) {
|
||||
for (const [k, v] of Object.entries(parsed.answers)) {
|
||||
answerMap[k] = String(v)
|
||||
}
|
||||
}
|
||||
} catch { /* not JSON */ }
|
||||
|
||||
return answerMap
|
||||
})
|
||||
|
||||
// Check if an option was selected for a given question
|
||||
function isSelected(questionText: string, optionLabel: string): boolean {
|
||||
const ans = answer.value[questionText]
|
||||
if (!ans) return false
|
||||
return ans.split(', ').includes(optionLabel)
|
||||
}
|
||||
|
||||
// Get user notes from result (text after "user notes:")
|
||||
const userNotes = computed<string>(() => {
|
||||
if (!props.call.result?.content) return ''
|
||||
const match = props.call.result.content.match(/user notes:\s*(.+?)\.?\s*(?:You can|$)/i)
|
||||
return match ? match[1].trim() : ''
|
||||
})
|
||||
|
||||
const isError = computed(() => props.call.result?.isError ?? false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['question-card', { error: isError }]">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">
|
||||
<svg 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>
|
||||
<span class="card-label">AskUserQuestion</span>
|
||||
<span v-if="isError" class="error-badge">error</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div v-for="(q, qi) in questions" :key="qi" class="question-block">
|
||||
<div class="question-text">
|
||||
<span v-if="q.header" class="question-tag">{{ q.header }}</span>
|
||||
{{ q.question }}
|
||||
<span v-if="q.multiSelect" class="multi-hint">(multiple)</span>
|
||||
</div>
|
||||
|
||||
<div v-if="q.options?.length" class="options-grid">
|
||||
<div
|
||||
v-for="opt in q.options"
|
||||
:key="opt.label"
|
||||
:class="['option-item', { selected: isSelected(q.question, opt.label) }]"
|
||||
>
|
||||
<span class="option-check">
|
||||
<svg v-if="isSelected(q.question, opt.label)" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="option-content">
|
||||
<span class="option-label">{{ opt.label }}</span>
|
||||
<span v-if="opt.description" class="option-desc">{{ opt.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show answer if it's custom text (not matching any option) -->
|
||||
<div v-if="answer[q.question] && !q.options?.some(o => isSelected(q.question, o.label))" class="custom-answer">
|
||||
<span class="answer-label">Answer:</span>
|
||||
<span class="answer-text">{{ answer[q.question] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User notes -->
|
||||
<div v-if="userNotes" class="user-notes">
|
||||
<span class="notes-label">Notes:</span>
|
||||
<span class="notes-text">{{ userNotes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result status -->
|
||||
<div v-if="call.result" :class="['card-footer', { error: isError }]">
|
||||
<span class="result-icon">{{ isError ? '✗' : '✓' }}</span>
|
||||
<span class="result-label">{{ isError ? 'Error' : 'Answered' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.question-card {
|
||||
border: 1px solid rgba(14, 165, 233, 0.25);
|
||||
border-left: 3px solid #0ea5e9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.question-card.error {
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
background: rgba(14, 165, 233, 0.06);
|
||||
border-bottom: 1px solid rgba(14, 165, 233, 0.12);
|
||||
}
|
||||
|
||||
.question-card.error .card-header {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-bottom-color: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #0ea5e9;
|
||||
}
|
||||
|
||||
.question-card.error .card-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #0ea5e9;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.question-card.error .card-label {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.error-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.6rem 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.question-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.question-block + .question-block {
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.question-text {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.question-tag {
|
||||
display: inline-block;
|
||||
background: rgba(14, 165, 233, 0.12);
|
||||
color: #0ea5e9;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
margin-right: 0.3rem;
|
||||
vertical-align: middle;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.multi-hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.options-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.option-item.selected {
|
||||
border-color: #0ea5e9;
|
||||
background: rgba(14, 165, 233, 0.08);
|
||||
}
|
||||
|
||||
.option-check {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 1px;
|
||||
border: 1.5px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.option-item.selected .option-check {
|
||||
background: #0ea5e9;
|
||||
border-color: #0ea5e9;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.option-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.option-desc {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.custom-answer {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.4rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: rgba(14, 165, 233, 0.06);
|
||||
border: 1px solid rgba(14, 165, 233, 0.15);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.answer-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #0ea5e9;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.answer-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.user-notes {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.4rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
background: rgba(251, 191, 36, 0.06);
|
||||
border: 1px solid rgba(251, 191, 36, 0.15);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.notes-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #fbbf24;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.notes-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border-top: 1px solid rgba(14, 165, 233, 0.12);
|
||||
background: rgba(34, 197, 94, 0.04);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.card-footer.error {
|
||||
background: rgba(239, 68, 68, 0.04);
|
||||
border-top-color: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
font-weight: 600;
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.card-footer.error .result-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
164
frontend/src/components/transcript-debug/toolCards/BashCard.vue
Normal file
164
frontend/src/components/transcript-debug/toolCards/BashCard.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import type { ParsedToolCall } from '@/types/transcript-debug'
|
||||
import { highlightCode } from '@/utils/markdown'
|
||||
import ToolResultBlock from '../ToolResultBlock.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
call: ParsedToolCall
|
||||
}>()
|
||||
|
||||
const command = computed(() => (props.call.input?.command as string) || '')
|
||||
const description = computed(() => (props.call.input?.description as string) || '')
|
||||
const timeout = computed(() => props.call.input?.timeout as number | undefined)
|
||||
const runInBackground = computed(() => props.call.input?.run_in_background as boolean | undefined)
|
||||
|
||||
const isError = computed(() => props.call.result?.isError ?? false)
|
||||
|
||||
const highlightedCommand = computed(() => highlightCode(command.value, 'bash'))
|
||||
|
||||
const outputExpanded = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['bash-card', { error: isError }]">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="4 17 10 11 4 5"/>
|
||||
<line x1="12" y1="19" x2="20" y2="19"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="card-label">Bash</span>
|
||||
<span v-if="runInBackground" class="bg-badge">background</span>
|
||||
<span v-if="timeout" class="timeout-badge">{{ timeout }}ms</span>
|
||||
<span v-if="isError" class="error-badge">error</span>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div v-if="description" class="description">{{ description }}</div>
|
||||
|
||||
<!-- Command -->
|
||||
<div class="command-section">
|
||||
<div class="command-prompt">$</div>
|
||||
<pre class="command-text" v-html="highlightedCommand"></pre>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<ToolResultBlock v-if="call.result" :result="call.result" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bash-card {
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 3px solid #f59e0b;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.bash-card.error {
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
background: rgba(245, 158, 11, 0.06);
|
||||
border-bottom: 1px solid rgba(245, 158, 11, 0.12);
|
||||
}
|
||||
|
||||
.bash-card.error .card-header {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-bottom-color: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.bash-card.error .card-icon { color: #ef4444; }
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #f59e0b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.bash-card.error .card-label { color: #ef4444; }
|
||||
|
||||
.bg-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
color: #818cf8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.timeout-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-muted);
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.error-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.command-section {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.command-prompt {
|
||||
color: #f59e0b;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.command-text {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
286
frontend/src/components/transcript-debug/toolCards/EditCard.vue
Normal file
286
frontend/src/components/transcript-debug/toolCards/EditCard.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import type { ParsedToolCall } from '@/types/transcript-debug'
|
||||
import { highlightCode } from '@/utils/markdown'
|
||||
import ToolResultBlock from '../ToolResultBlock.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
call: ParsedToolCall
|
||||
}>()
|
||||
|
||||
const filePath = computed(() => (props.call.input?.file_path as string) || '')
|
||||
const fileName = computed(() => {
|
||||
const fp = filePath.value
|
||||
if (!fp) return ''
|
||||
const parts = fp.replace(/\\/g, '/').split('/')
|
||||
return parts[parts.length - 1]
|
||||
})
|
||||
const oldString = computed(() => (props.call.input?.old_string as string) || '')
|
||||
const newString = computed(() => (props.call.input?.new_string as string) || '')
|
||||
const replaceAll = computed(() => props.call.input?.replace_all as boolean | undefined)
|
||||
|
||||
const ext = computed(() => {
|
||||
const name = fileName.value
|
||||
const dot = name.lastIndexOf('.')
|
||||
return dot >= 0 ? name.slice(dot + 1).toLowerCase() : ''
|
||||
})
|
||||
|
||||
const isError = computed(() => props.call.result?.isError ?? false)
|
||||
|
||||
const diffExpanded = ref(true)
|
||||
|
||||
const highlightedOld = computed(() => highlightCode(oldString.value, ext.value || undefined))
|
||||
const highlightedNew = computed(() => highlightCode(newString.value, ext.value || undefined))
|
||||
|
||||
const oldLineCount = computed(() => oldString.value.split('\n').length)
|
||||
const newLineCount = computed(() => newString.value.split('\n').length)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['edit-card', { error: isError }]">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="card-label">Edit</span>
|
||||
<span v-if="ext" class="ext-badge">.{{ ext }}</span>
|
||||
<span v-if="replaceAll" class="replace-all-badge">replace all</span>
|
||||
<span v-if="isError" class="error-badge">error</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="file-path" :title="filePath">
|
||||
<span class="path-dir">{{ filePath.replace(/\\/g, '/').split('/').slice(0, -1).join('/') }}/</span>
|
||||
<span class="path-file">{{ fileName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diff view -->
|
||||
<div class="diff-section">
|
||||
<button class="diff-toggle" @click="diffExpanded = !diffExpanded">
|
||||
<svg
|
||||
:class="['chevron', { rotated: diffExpanded }]"
|
||||
width="10" height="10" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
<span>Changes</span>
|
||||
<span class="diff-meta">-{{ oldLineCount }} +{{ newLineCount }} lines</span>
|
||||
</button>
|
||||
|
||||
<div v-if="diffExpanded" class="diff-content">
|
||||
<!-- Old string (removed) -->
|
||||
<div class="diff-block removed">
|
||||
<div class="diff-label">
|
||||
<span class="diff-sign">-</span>
|
||||
<span>old</span>
|
||||
</div>
|
||||
<pre class="diff-code" v-html="highlightedOld"></pre>
|
||||
</div>
|
||||
|
||||
<!-- New string (added) -->
|
||||
<div class="diff-block added">
|
||||
<div class="diff-label">
|
||||
<span class="diff-sign">+</span>
|
||||
<span>new</span>
|
||||
</div>
|
||||
<pre class="diff-code" v-html="highlightedNew"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<ToolResultBlock v-if="call.result" :result="call.result" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.edit-card {
|
||||
border: 1px solid rgba(99, 102, 241, 0.25);
|
||||
border-left: 3px solid #6366f1;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.edit-card.error {
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
background: rgba(99, 102, 241, 0.06);
|
||||
border-bottom: 1px solid rgba(99, 102, 241, 0.12);
|
||||
}
|
||||
|
||||
.edit-card.error .card-header {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-bottom-color: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.edit-card.error .card-icon { color: #ef4444; }
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #6366f1;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.edit-card.error .card-label { color: #ef4444; }
|
||||
|
||||
.ext-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
color: #818cf8;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.replace-all-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(251, 191, 36, 0.12);
|
||||
color: #fbbf24;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.file-path {
|
||||
font-size: 12px;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.path-dir { color: var(--text-muted); }
|
||||
.path-file { color: var(--text-primary); font-weight: 600; }
|
||||
|
||||
/* Diff section */
|
||||
.diff-section {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.diff-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
width: 100%;
|
||||
padding: 0.35rem 0.75rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.diff-toggle:hover { background: var(--bg-hover); }
|
||||
|
||||
.diff-meta {
|
||||
margin-left: auto;
|
||||
color: var(--text-muted);
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chevron.rotated { transform: rotate(90deg); }
|
||||
|
||||
.diff-content {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.diff-block {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.diff-block:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.diff-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.2rem 0.75rem;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.diff-block.removed .diff-label {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.diff-block.added .diff-label {
|
||||
background: rgba(34, 197, 94, 0.06);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.diff-sign {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
.diff-code {
|
||||
margin: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
.diff-block.removed .diff-code {
|
||||
background: rgba(239, 68, 68, 0.03);
|
||||
}
|
||||
|
||||
.diff-block.added .diff-code {
|
||||
background: rgba(34, 197, 94, 0.03);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,148 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { ParsedToolCall } from '@/types/transcript-debug'
|
||||
|
||||
const props = defineProps<{
|
||||
call: ParsedToolCall
|
||||
}>()
|
||||
|
||||
const isApproved = computed(() => {
|
||||
if (!props.call.result?.content) return false
|
||||
return props.call.result.content.toLowerCase().includes('entered plan mode') ||
|
||||
props.call.result.content.toLowerCase().includes('approved')
|
||||
})
|
||||
|
||||
const isError = computed(() => props.call.result?.isError ?? false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['enter-plan-card', { error: isError }]">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="12"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="card-label">EnterPlanMode</span>
|
||||
<span v-if="isApproved && !isError" class="status-badge approved">entered</span>
|
||||
<span v-else-if="isError" class="status-badge error">error</span>
|
||||
<span v-else-if="call.result" class="status-badge denied">denied</span>
|
||||
</div>
|
||||
|
||||
<div v-if="call.result" :class="['card-footer', { approved: isApproved && !isError, error: isError }]">
|
||||
<span class="result-icon">{{ isError ? '✗' : isApproved ? '→' : '⊘' }}</span>
|
||||
<span class="result-text">{{ isApproved ? 'Entered plan mode' : isError ? 'Error' : 'Denied' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.enter-plan-card {
|
||||
border: 1px solid rgba(251, 191, 36, 0.25);
|
||||
border-left: 3px solid #fbbf24;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.enter-plan-card.error {
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
background: rgba(251, 191, 36, 0.06);
|
||||
}
|
||||
|
||||
.enter-plan-card.error .card-header {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.enter-plan-card.error .card-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #fbbf24;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.enter-plan-card.error .card-label {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.status-badge.approved {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.status-badge.denied {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.status-badge.error {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-size: 11px;
|
||||
background: rgba(251, 191, 36, 0.03);
|
||||
}
|
||||
|
||||
.card-footer.approved {
|
||||
background: rgba(34, 197, 94, 0.04);
|
||||
}
|
||||
|
||||
.card-footer.error {
|
||||
background: rgba(239, 68, 68, 0.04);
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.card-footer.approved .result-icon {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.card-footer.error .result-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.result-text {
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,267 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import type { ParsedToolCall } from '@/types/transcript-debug'
|
||||
import MarkdownContent from '../MarkdownContent.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
call: ParsedToolCall
|
||||
}>()
|
||||
|
||||
const expanded = ref(false)
|
||||
|
||||
const planText = computed(() => {
|
||||
if (!props.call.input) return ''
|
||||
// ExitPlanMode may have plan in input or allowedPrompts
|
||||
return (props.call.input.plan as string) || ''
|
||||
})
|
||||
|
||||
const allowedPrompts = computed(() => {
|
||||
const prompts = props.call.input?.allowedPrompts
|
||||
if (!Array.isArray(prompts)) return []
|
||||
return prompts as Array<{ tool: string; prompt: string }>
|
||||
})
|
||||
|
||||
const isApproved = computed(() => {
|
||||
if (!props.call.result?.content) return false
|
||||
return props.call.result.content.toLowerCase().includes('approved') ||
|
||||
props.call.result.content.toLowerCase().includes('allow')
|
||||
})
|
||||
|
||||
const isError = computed(() => props.call.result?.isError ?? false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['plan-card', { error: isError }]">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">
|
||||
<svg 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>
|
||||
<span class="card-label">ExitPlanMode</span>
|
||||
<span v-if="isApproved && !isError" class="approved-badge">approved</span>
|
||||
<span v-if="isError" class="error-badge">error</span>
|
||||
</div>
|
||||
|
||||
<div v-if="planText" class="card-body">
|
||||
<button class="plan-toggle" @click="expanded = !expanded">
|
||||
<svg
|
||||
:class="['chevron', { rotated: expanded }]"
|
||||
width="10" height="10" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
<span>Plan</span>
|
||||
<span class="plan-size">{{ planText.length }} chars</span>
|
||||
</button>
|
||||
<div v-if="expanded" class="plan-content">
|
||||
<MarkdownContent :content="planText" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Allowed prompts -->
|
||||
<div v-if="allowedPrompts.length" class="prompts-section">
|
||||
<div class="prompts-title">Allowed Prompts</div>
|
||||
<div v-for="(p, i) in allowedPrompts" :key="i" class="prompt-item">
|
||||
<code class="prompt-tool">{{ p.tool }}</code>
|
||||
<span class="prompt-text">{{ p.prompt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result status -->
|
||||
<div v-if="call.result" :class="['card-footer', { error: isError, approved: isApproved && !isError }]">
|
||||
<span class="result-icon">{{ isError ? '✗' : isApproved ? '✓' : '⊘' }}</span>
|
||||
<span class="result-label">{{ isError ? 'Error' : isApproved ? 'Plan Approved' : 'Rejected' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.plan-card {
|
||||
border: 1px solid rgba(139, 92, 246, 0.25);
|
||||
border-left: 3px solid #8b5cf6;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.plan-card.error {
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
background: rgba(139, 92, 246, 0.06);
|
||||
border-bottom: 1px solid rgba(139, 92, 246, 0.12);
|
||||
}
|
||||
|
||||
.plan-card.error .card-header {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-bottom-color: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.plan-card.error .card-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #8b5cf6;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.plan-card.error .card-label {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.approved-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.plan-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
width: 100%;
|
||||
padding: 0.35rem 0.75rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.plan-toggle:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.plan-size {
|
||||
margin-left: auto;
|
||||
color: var(--text-muted);
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chevron.rotated {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.plan-content {
|
||||
padding: 0.6rem 0.75rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.prompts-section {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.prompts-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.prompt-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.2rem 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.prompt-tool {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
color: #8b5cf6;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.prompt-text {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.3rem 0.75rem;
|
||||
font-size: 11px;
|
||||
background: rgba(239, 68, 68, 0.04);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.card-footer.approved {
|
||||
background: rgba(34, 197, 94, 0.04);
|
||||
}
|
||||
|
||||
.card-footer.error {
|
||||
background: rgba(239, 68, 68, 0.04);
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.card-footer.approved .result-icon {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.card-footer.error .result-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
170
frontend/src/components/transcript-debug/toolCards/GlobCard.vue
Normal file
170
frontend/src/components/transcript-debug/toolCards/GlobCard.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { ParsedToolCall } from '@/types/transcript-debug'
|
||||
import ToolResultBlock from '../ToolResultBlock.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
call: ParsedToolCall
|
||||
}>()
|
||||
|
||||
const pattern = computed(() => (props.call.input?.pattern as string) || '')
|
||||
const path = computed(() => (props.call.input?.path as string) || '')
|
||||
|
||||
const isError = computed(() => props.call.result?.isError ?? false)
|
||||
|
||||
// Count files from result
|
||||
const fileCount = computed(() => {
|
||||
if (!props.call.result?.content) return null
|
||||
const content = props.call.result.content.trim()
|
||||
if (!content) return 0
|
||||
return content.split('\n').filter(l => l.trim()).length
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['glob-card', { error: isError }]">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="card-label">Glob</span>
|
||||
<span v-if="fileCount != null && !isError" class="match-count">{{ fileCount }} files</span>
|
||||
<span v-if="isError" class="error-badge">error</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Pattern -->
|
||||
<div class="pattern-row">
|
||||
<code class="pattern-text">{{ pattern }}</code>
|
||||
</div>
|
||||
|
||||
<!-- Path scope -->
|
||||
<div v-if="path" class="scope-row">
|
||||
<span class="scope-badge">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
{{ path.replace(/\\/g, '/').split('/').slice(-3).join('/') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<ToolResultBlock v-if="call.result" :result="call.result" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.glob-card {
|
||||
border: 1px solid rgba(251, 191, 36, 0.25);
|
||||
border-left: 3px solid #fbbf24;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.glob-card.error {
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
background: rgba(251, 191, 36, 0.06);
|
||||
border-bottom: 1px solid rgba(251, 191, 36, 0.12);
|
||||
}
|
||||
|
||||
.glob-card.error .card-header {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-bottom-color: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.glob-card.error .card-icon { color: #ef4444; }
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #fbbf24;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.glob-card.error .card-label { color: #ef4444; }
|
||||
|
||||
.match-count {
|
||||
margin-left: auto;
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #22c55e;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.5rem 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.pattern-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pattern-text {
|
||||
font-size: 12px;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
color: #fbbf24;
|
||||
background: rgba(251, 191, 36, 0.06);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(251, 191, 36, 0.12);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.scope-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.scope-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
background: rgba(6, 182, 212, 0.08);
|
||||
color: #06b6d4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 280px;
|
||||
}
|
||||
</style>
|
||||
228
frontend/src/components/transcript-debug/toolCards/GrepCard.vue
Normal file
228
frontend/src/components/transcript-debug/toolCards/GrepCard.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import type { ParsedToolCall } from '@/types/transcript-debug'
|
||||
import ToolResultBlock from '../ToolResultBlock.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
call: ParsedToolCall
|
||||
}>()
|
||||
|
||||
const pattern = computed(() => (props.call.input?.pattern as string) || '')
|
||||
const path = computed(() => (props.call.input?.path as string) || '')
|
||||
const glob = computed(() => (props.call.input?.glob as string) || '')
|
||||
const type = computed(() => (props.call.input?.type as string) || '')
|
||||
const outputMode = computed(() => (props.call.input?.output_mode as string) || 'files_with_matches')
|
||||
const caseInsensitive = computed(() => props.call.input?.['-i'] as boolean | undefined)
|
||||
const multiline = computed(() => props.call.input?.multiline as boolean | undefined)
|
||||
const headLimit = computed(() => props.call.input?.head_limit as number | undefined)
|
||||
const context = computed(() => {
|
||||
const c = props.call.input?.context ?? props.call.input?.['-C']
|
||||
const a = props.call.input?.['-A']
|
||||
const b = props.call.input?.['-B']
|
||||
if (c != null) return `C${c}`
|
||||
if (a != null && b != null) return `B${b}/A${a}`
|
||||
if (a != null) return `A${a}`
|
||||
if (b != null) return `B${b}`
|
||||
return ''
|
||||
})
|
||||
|
||||
const isError = computed(() => props.call.result?.isError ?? false)
|
||||
|
||||
// Count matches from result
|
||||
const matchCount = computed(() => {
|
||||
if (!props.call.result?.content) return null
|
||||
const content = props.call.result.content.trim()
|
||||
if (!content) return 0
|
||||
return content.split('\n').filter(l => l.trim()).length
|
||||
})
|
||||
|
||||
const detailsExpanded = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['grep-card', { error: isError }]">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="card-label">Grep</span>
|
||||
<span class="mode-badge">{{ outputMode }}</span>
|
||||
<span v-if="matchCount != null && !isError" class="match-count">{{ matchCount }} matches</span>
|
||||
<span v-if="isError" class="error-badge">error</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Pattern -->
|
||||
<div class="pattern-row">
|
||||
<code class="pattern-text">/{{ pattern }}/{{ caseInsensitive ? 'i' : '' }}{{ multiline ? 'm' : '' }}</code>
|
||||
</div>
|
||||
|
||||
<!-- Search scope -->
|
||||
<div class="scope-row">
|
||||
<span v-if="path" class="scope-badge path" :title="path">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
{{ path.replace(/\\/g, '/').split('/').slice(-2).join('/') }}
|
||||
</span>
|
||||
<span v-if="glob" class="scope-badge glob">{{ glob }}</span>
|
||||
<span v-if="type" class="scope-badge type">{{ type }}</span>
|
||||
<span v-if="context" class="scope-badge ctx">{{ context }}</span>
|
||||
<span v-if="headLimit" class="scope-badge limit">head {{ headLimit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<ToolResultBlock v-if="call.result" :result="call.result" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.grep-card {
|
||||
border: 1px solid rgba(236, 72, 153, 0.25);
|
||||
border-left: 3px solid #ec4899;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.grep-card.error {
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
background: rgba(236, 72, 153, 0.06);
|
||||
border-bottom: 1px solid rgba(236, 72, 153, 0.12);
|
||||
}
|
||||
|
||||
.grep-card.error .card-header {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-bottom-color: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #ec4899;
|
||||
}
|
||||
|
||||
.grep-card.error .card-icon { color: #ef4444; }
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #ec4899;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.grep-card.error .card-label { color: #ef4444; }
|
||||
|
||||
.mode-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(236, 72, 153, 0.1);
|
||||
color: #ec4899;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.match-count {
|
||||
margin-left: auto;
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #22c55e;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.5rem 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.pattern-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pattern-text {
|
||||
font-size: 12px;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
color: #f472b6;
|
||||
background: rgba(236, 72, 153, 0.06);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(236, 72, 153, 0.12);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.scope-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.scope-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.scope-badge.path {
|
||||
background: rgba(6, 182, 212, 0.08);
|
||||
color: #06b6d4;
|
||||
}
|
||||
|
||||
.scope-badge.glob {
|
||||
background: rgba(251, 191, 36, 0.08);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.scope-badge.type {
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
color: #818cf8;
|
||||
}
|
||||
|
||||
.scope-badge.ctx {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.scope-badge.limit {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
169
frontend/src/components/transcript-debug/toolCards/ReadCard.vue
Normal file
169
frontend/src/components/transcript-debug/toolCards/ReadCard.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import type { ParsedToolCall } from '@/types/transcript-debug'
|
||||
import ToolResultBlock from '../ToolResultBlock.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
call: ParsedToolCall
|
||||
}>()
|
||||
|
||||
const filePath = computed(() => (props.call.input?.file_path as string) || '')
|
||||
const fileName = computed(() => {
|
||||
const fp = filePath.value
|
||||
if (!fp) return ''
|
||||
const parts = fp.replace(/\\/g, '/').split('/')
|
||||
return parts[parts.length - 1]
|
||||
})
|
||||
const offset = computed(() => props.call.input?.offset as number | undefined)
|
||||
const limit = computed(() => props.call.input?.limit as number | undefined)
|
||||
const pages = computed(() => props.call.input?.pages as string | undefined)
|
||||
|
||||
const isError = computed(() => props.call.result?.isError ?? false)
|
||||
|
||||
// Detect file extension for icon hint
|
||||
const ext = computed(() => {
|
||||
const name = fileName.value
|
||||
const dot = name.lastIndexOf('.')
|
||||
return dot >= 0 ? name.slice(dot + 1).toLowerCase() : ''
|
||||
})
|
||||
|
||||
const resultExpanded = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['read-card', { error: isError }]">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">
|
||||
<svg 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"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="card-label">Read</span>
|
||||
<span v-if="ext" class="ext-badge">.{{ ext }}</span>
|
||||
<span v-if="isError" class="error-badge">error</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="file-path" :title="filePath">
|
||||
<span class="path-dir">{{ filePath.replace(/\\/g, '/').split('/').slice(0, -1).join('/') }}/</span>
|
||||
<span class="path-file">{{ fileName }}</span>
|
||||
</div>
|
||||
<div v-if="offset != null || limit != null || pages" class="range-info">
|
||||
<span v-if="offset != null" class="range-badge">offset: {{ offset }}</span>
|
||||
<span v-if="limit != null" class="range-badge">limit: {{ limit }}</span>
|
||||
<span v-if="pages" class="range-badge">pages: {{ pages }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<ToolResultBlock v-if="call.result" :result="call.result" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-card {
|
||||
border: 1px solid rgba(6, 182, 212, 0.25);
|
||||
border-left: 3px solid #06b6d4;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.read-card.error {
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
background: rgba(6, 182, 212, 0.06);
|
||||
border-bottom: 1px solid rgba(6, 182, 212, 0.12);
|
||||
}
|
||||
|
||||
.read-card.error .card-header {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-bottom-color: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #06b6d4;
|
||||
}
|
||||
|
||||
.read-card.error .card-icon { color: #ef4444; }
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #06b6d4;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.read-card.error .card-label { color: #ef4444; }
|
||||
|
||||
.ext-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(6, 182, 212, 0.12);
|
||||
color: #06b6d4;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.5rem 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.file-path {
|
||||
font-size: 12px;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.path-dir {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.path-file {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.range-info {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.range-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-muted);
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
}
|
||||
</style>
|
||||
357
frontend/src/components/transcript-debug/toolCards/TaskCard.vue
Normal file
357
frontend/src/components/transcript-debug/toolCards/TaskCard.vue
Normal file
@@ -0,0 +1,357 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import type { ParsedToolCall } from '@/types/transcript-debug'
|
||||
import ToolResultBlock from '../ToolResultBlock.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
call: ParsedToolCall
|
||||
}>()
|
||||
|
||||
const isError = computed(() => props.call.result?.isError ?? false)
|
||||
|
||||
// Detect which Task-family tool this is
|
||||
const toolName = computed(() => props.call.name)
|
||||
|
||||
// Common fields across Task tools
|
||||
const taskId = computed(() => (props.call.input?.taskId as string) || '')
|
||||
const subject = computed(() => (props.call.input?.subject as string) || '')
|
||||
const description = computed(() => (props.call.input?.description as string) || '')
|
||||
const activeForm = computed(() => (props.call.input?.activeForm as string) || '')
|
||||
const status = computed(() => (props.call.input?.status as string) || '')
|
||||
const prompt = computed(() => (props.call.input?.prompt as string) || '')
|
||||
const subagentType = computed(() => (props.call.input?.subagent_type as string) || '')
|
||||
|
||||
// For Task (subagent launcher)
|
||||
const taskDescription = computed(() => (props.call.input?.description as string) || '')
|
||||
const model = computed(() => (props.call.input?.model as string) || '')
|
||||
const runInBackground = computed(() => props.call.input?.run_in_background as boolean | undefined)
|
||||
|
||||
// Status styling
|
||||
const statusColor = computed(() => {
|
||||
switch (status.value) {
|
||||
case 'completed': return '#22c55e'
|
||||
case 'in_progress': return '#f59e0b'
|
||||
case 'pending': return '#64748b'
|
||||
case 'deleted': return '#ef4444'
|
||||
default: return '#64748b'
|
||||
}
|
||||
})
|
||||
|
||||
const statusIcon = computed(() => {
|
||||
switch (status.value) {
|
||||
case 'completed': return '✓'
|
||||
case 'in_progress': return '→'
|
||||
case 'pending': return '○'
|
||||
case 'deleted': return '✗'
|
||||
default: return '•'
|
||||
}
|
||||
})
|
||||
|
||||
// Card color based on tool
|
||||
const cardColor = computed(() => {
|
||||
switch (toolName.value) {
|
||||
case 'Task': return '#0ea5e9' // blue - agent launch
|
||||
case 'TaskCreate': return '#22c55e' // green - create
|
||||
case 'TaskUpdate': return '#a855f7' // purple - update
|
||||
case 'TaskGet': return '#06b6d4' // cyan - read
|
||||
case 'TaskList': return '#64748b' // gray - list
|
||||
default: return '#64748b'
|
||||
}
|
||||
})
|
||||
|
||||
const cardIcon = computed(() => toolName.value)
|
||||
|
||||
const descExpanded = ref(false)
|
||||
const promptExpanded = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['task-card', { error: isError }]" :style="{ '--card-color': cardColor }">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">
|
||||
<!-- Task (subagent) -->
|
||||
<svg v-if="toolName === 'Task'" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||
</svg>
|
||||
<!-- TaskCreate -->
|
||||
<svg v-else-if="toolName === 'TaskCreate'" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="16"/>
|
||||
<line x1="8" y1="12" x2="16" y2="12"/>
|
||||
</svg>
|
||||
<!-- TaskUpdate -->
|
||||
<svg v-else-if="toolName === 'TaskUpdate'" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 11l3 3L22 4"/>
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
|
||||
</svg>
|
||||
<!-- TaskGet -->
|
||||
<svg v-else-if="toolName === 'TaskGet'" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<!-- TaskList -->
|
||||
<svg v-else width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="8" y1="6" x2="21" y2="6"/>
|
||||
<line x1="8" y1="12" x2="21" y2="12"/>
|
||||
<line x1="8" y1="18" x2="21" y2="18"/>
|
||||
<line x1="3" y1="6" x2="3.01" y2="6"/>
|
||||
<line x1="3" y1="12" x2="3.01" y2="12"/>
|
||||
<line x1="3" y1="18" x2="3.01" y2="18"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="card-label">{{ toolName }}</span>
|
||||
<span v-if="taskId" class="id-badge">#{{ taskId }}</span>
|
||||
<span v-if="subagentType" class="agent-badge">{{ subagentType }}</span>
|
||||
<span v-if="model" class="model-badge">{{ model }}</span>
|
||||
<span v-if="runInBackground" class="bg-badge">background</span>
|
||||
<span v-if="isError" class="error-badge">error</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Status (TaskUpdate) -->
|
||||
<div v-if="status" class="field-row">
|
||||
<span class="field-label">Status</span>
|
||||
<span class="status-badge" :style="{ color: statusColor, borderColor: statusColor, background: statusColor + '15' }">
|
||||
<span class="status-icon">{{ statusIcon }}</span>
|
||||
{{ status }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Subject (TaskCreate/TaskUpdate) -->
|
||||
<div v-if="subject" class="field-row">
|
||||
<span class="field-label">Subject</span>
|
||||
<span class="field-value subject">{{ subject }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Active form -->
|
||||
<div v-if="activeForm" class="field-row">
|
||||
<span class="field-label">Active</span>
|
||||
<span class="field-value active-form">{{ activeForm }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Description (collapsible for long text) -->
|
||||
<div v-if="description && description.length <= 120" class="field-row">
|
||||
<span class="field-label">Desc</span>
|
||||
<span class="field-value desc-text">{{ description }}</span>
|
||||
</div>
|
||||
<div v-else-if="description" class="collapsible-section">
|
||||
<button class="section-toggle" @click="descExpanded = !descExpanded">
|
||||
<svg :class="['chevron', { rotated: descExpanded }]" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
<span>Description</span>
|
||||
<span class="section-meta">{{ description.length }} chars</span>
|
||||
</button>
|
||||
<pre v-if="descExpanded" class="section-content">{{ description }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Prompt (Task subagent) -->
|
||||
<div v-if="prompt && prompt.length <= 120" class="field-row">
|
||||
<span class="field-label">Prompt</span>
|
||||
<span class="field-value desc-text">{{ prompt }}</span>
|
||||
</div>
|
||||
<div v-else-if="prompt" class="collapsible-section">
|
||||
<button class="section-toggle" @click="promptExpanded = !promptExpanded">
|
||||
<svg :class="['chevron', { rotated: promptExpanded }]" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
<span>Prompt</span>
|
||||
<span class="section-meta">{{ prompt.length }} chars</span>
|
||||
</button>
|
||||
<pre v-if="promptExpanded" class="section-content">{{ prompt }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<ToolResultBlock v-if="call.result" :result="call.result" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.task-card {
|
||||
border: 1px solid color-mix(in srgb, var(--card-color) 25%, transparent);
|
||||
border-left: 3px solid var(--card-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.task-card.error {
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
background: color-mix(in srgb, var(--card-color) 6%, transparent);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--card-color) 12%, transparent);
|
||||
}
|
||||
|
||||
.task-card.error .card-header {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-bottom-color: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--card-color);
|
||||
}
|
||||
|
||||
.task-card.error .card-icon { color: #ef4444; }
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--card-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.task-card.error .card-label { color: #ef4444; }
|
||||
|
||||
.id-badge {
|
||||
font-size: 11px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
background: color-mix(in srgb, var(--card-color) 12%, transparent);
|
||||
color: var(--card-color);
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.agent-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
color: #818cf8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.model-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
color: var(--accent, #6366f1);
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
.bg-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
color: #818cf8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.5rem 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
min-width: 45px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 0.15rem 0.45rem;
|
||||
border: 1px solid;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.status-icon { font-size: 12px; }
|
||||
|
||||
.field-value {
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.field-value.subject { font-weight: 500; }
|
||||
.field-value.active-form { font-style: italic; color: var(--text-secondary); }
|
||||
.field-value.desc-text { font-size: 11px; color: var(--text-muted); line-height: 1.3; }
|
||||
|
||||
/* Collapsible sections */
|
||||
.collapsible-section {
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: 0 -0.75rem;
|
||||
}
|
||||
|
||||
.section-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
width: 100%;
|
||||
padding: 0.3rem 0.75rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.section-toggle:hover { background: var(--bg-hover); }
|
||||
|
||||
.section-meta {
|
||||
margin-left: auto;
|
||||
color: var(--text-muted);
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chevron.rotated { transform: rotate(90deg); }
|
||||
|
||||
.section-content {
|
||||
margin: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
}
|
||||
</style>
|
||||
210
frontend/src/components/transcript-debug/toolCards/WriteCard.vue
Normal file
210
frontend/src/components/transcript-debug/toolCards/WriteCard.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import type { ParsedToolCall } from '@/types/transcript-debug'
|
||||
import { highlightCode } from '@/utils/markdown'
|
||||
import ToolResultBlock from '../ToolResultBlock.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
call: ParsedToolCall
|
||||
}>()
|
||||
|
||||
const filePath = computed(() => (props.call.input?.file_path as string) || '')
|
||||
const fileName = computed(() => {
|
||||
const fp = filePath.value
|
||||
if (!fp) return ''
|
||||
const parts = fp.replace(/\\/g, '/').split('/')
|
||||
return parts[parts.length - 1]
|
||||
})
|
||||
const content = computed(() => (props.call.input?.content as string) || '')
|
||||
|
||||
const ext = computed(() => {
|
||||
const name = fileName.value
|
||||
const dot = name.lastIndexOf('.')
|
||||
return dot >= 0 ? name.slice(dot + 1).toLowerCase() : ''
|
||||
})
|
||||
|
||||
const isError = computed(() => props.call.result?.isError ?? false)
|
||||
|
||||
const contentExpanded = ref(false)
|
||||
|
||||
const highlightedContent = computed(() => highlightCode(content.value, ext.value || undefined))
|
||||
|
||||
const lineCount = computed(() => content.value.split('\n').length)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['write-card', { error: isError }]">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">
|
||||
<svg 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="12" y1="18" x2="12" y2="12"/>
|
||||
<polyline points="9 15 12 12 15 15"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="card-label">Write</span>
|
||||
<span v-if="ext" class="ext-badge">.{{ ext }}</span>
|
||||
<span v-if="isError" class="error-badge">error</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="file-path" :title="filePath">
|
||||
<span class="path-dir">{{ filePath.replace(/\\/g, '/').split('/').slice(0, -1).join('/') }}/</span>
|
||||
<span class="path-file">{{ fileName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content preview -->
|
||||
<div class="content-section">
|
||||
<button class="content-toggle" @click="contentExpanded = !contentExpanded">
|
||||
<svg
|
||||
:class="['chevron', { rotated: contentExpanded }]"
|
||||
width="10" height="10" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
<span>Content</span>
|
||||
<span class="content-meta">{{ lineCount }} lines · {{ content.length }} chars</span>
|
||||
</button>
|
||||
<pre v-if="contentExpanded" class="content-pre" v-html="highlightedContent"></pre>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<ToolResultBlock v-if="call.result" :result="call.result" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.write-card {
|
||||
border: 1px solid rgba(34, 197, 94, 0.25);
|
||||
border-left: 3px solid #22c55e;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.write-card.error {
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
background: rgba(34, 197, 94, 0.06);
|
||||
border-bottom: 1px solid rgba(34, 197, 94, 0.12);
|
||||
}
|
||||
|
||||
.write-card.error .card-header {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-bottom-color: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.write-card.error .card-icon { color: #ef4444; }
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #22c55e;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.write-card.error .card-label { color: #ef4444; }
|
||||
|
||||
.ext-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(34, 197, 94, 0.12);
|
||||
color: #22c55e;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-badge {
|
||||
font-size: 10px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.file-path {
|
||||
font-size: 12px;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.path-dir { color: var(--text-muted); }
|
||||
.path-file { color: var(--text-primary); font-weight: 600; }
|
||||
|
||||
/* Content preview */
|
||||
.content-section {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.content-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
width: 100%;
|
||||
padding: 0.35rem 0.75rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.content-toggle:hover { background: var(--bg-hover); }
|
||||
|
||||
.content-meta {
|
||||
margin-left: auto;
|
||||
color: var(--text-muted);
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chevron.rotated { transform: rotate(90deg); }
|
||||
|
||||
.content-pre {
|
||||
margin: 0;
|
||||
padding: 0.6rem 0.75rem;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user