| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- <script setup lang="ts">
- import { computed, ref } from 'vue'
- import type { TimelineItem } from '../store'
- import { stringifyInput, toolLabel } from '../util'
- const props = defineProps<{ item: Extract<TimelineItem, { kind: 'tool' }> }>()
- // 失败或被拒的工具默认展开
- const open = ref(false)
- const expanded = computed(() => open.value || props.item.status === 'error')
- const full = ref(false)
- const inputText = computed(() => {
- const input = props.item.input as Record<string, unknown> | null
- if (input && typeof input === 'object' && typeof input.command === 'string') return input.command
- return stringifyInput(input)
- })
- </script>
- <template>
- <div class="tool">
- <button class="tool-head" type="button" :aria-expanded="expanded" @click="open = !open">
- <span class="tool-name">{{ toolLabel(item.name) }}</span>
- <span class="tool-title">{{ item.title }}</span>
- <span v-if="item.status === 'running'" class="codicon codicon-loading codicon-modifier-spin tool-spin" />
- <span v-else-if="item.status === 'denied'" class="tool-tag">已拒绝</span>
- <span class="codicon tool-chevron" :class="expanded ? 'codicon-chevron-down' : 'codicon-chevron-right'" />
- </button>
- <div v-if="expanded" class="tool-body">
- <div class="row">
- <div class="row-label">IN</div>
- <pre class="row-content" :class="{ clip: !full }" @click="full = true">{{ inputText }}</pre>
- </div>
- <div v-if="item.status !== 'running'" class="row">
- <div class="row-label">OUT</div>
- <pre class="row-content" :class="{ clip: !full, error: item.status !== 'ok' }" @click="full = true">{{ item.output || '(无输出)' }}<template v-if="item.truncated">
- …(输出过长,已截断)</template></pre>
- </div>
- </div>
- </div>
- </template>
- <style scoped>
- .tool { width: 100%; }
- .tool-head {
- display: flex;
- align-items: baseline;
- gap: 6px;
- width: 100%;
- padding: 0;
- border: none;
- background: none;
- cursor: pointer;
- text-align: left;
- color: var(--app-primary-foreground);
- }
- .tool-name { font-weight: 700; flex: none; }
- .tool-title {
- flex: 1;
- min-width: 0;
- font-family: var(--app-monospace-font-family);
- font-size: 0.85em;
- color: var(--app-secondary-foreground);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .tool-spin { font-size: 12px; color: var(--app-secondary-foreground); }
- .tool-tag { font-size: 11px; color: var(--app-danger); flex: none; }
- .tool-chevron { font-size: 12px; color: var(--app-secondary-foreground); opacity: 0; flex: none; }
- .tool-head:hover .tool-chevron, .tool-head[aria-expanded='true'] .tool-chevron { opacity: 1; }
- .tool-body {
- margin-top: 6px;
- display: grid;
- grid-template-columns: max-content 1fr;
- border: 1px solid var(--app-input-border);
- border-radius: var(--corner-radius-medium);
- background: var(--app-code-background);
- overflow: hidden;
- }
- .row { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; padding: 4px; border-top: 0.5px solid var(--app-input-border); }
- .row:first-child { border-top: none; }
- .row-label {
- grid-column: 1;
- font-family: var(--app-monospace-font-family);
- font-size: 0.85em;
- opacity: 0.5;
- padding: 4px 8px 4px 4px;
- }
- .row-content {
- grid-column: 2;
- margin: 0;
- padding: 4px;
- min-width: 0;
- white-space: pre-wrap;
- word-break: break-word;
- font-family: var(--app-monospace-font-family);
- font-size: var(--app-monospace-font-size);
- }
- .row-content.clip {
- max-height: 60px;
- overflow: hidden;
- cursor: pointer;
- mask-image: linear-gradient(to bottom, #000 40px, transparent 60px);
- }
- .row-content.error { color: var(--app-error-foreground); }
- </style>
|