feat: Animated pixel art ocean floor background for FloatingTranscriptDebug
Replace galaxy background with animated underwater scene featuring water depth gradient, pixel art sea floor (sand, seaweed, coral, starfish), rising bubbles with water sway, and swimming pixel art fish. Also update transcript-debug tool cards styling and add .claude-*/tasks/ to gitignore.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,3 +13,4 @@ server/recordings/*.webm
|
|||||||
.claude-*/plugins/
|
.claude-*/plugins/
|
||||||
.claude-*/plans/
|
.claude-*/plans/
|
||||||
.claude-*/file-history/
|
.claude-*/file-history/
|
||||||
|
.claude-*/tasks/
|
||||||
|
|||||||
@@ -665,9 +665,77 @@ onBeforeUnmount(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
/* Pixel art galaxy: spiral arms, stars, nebula in bottom-right corner */
|
isolation: isolate;
|
||||||
|
/* Pixel art animated ocean floor */
|
||||||
background:
|
background:
|
||||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120' shape-rendering='crispEdges'%3E%3C!-- galaxy core --%3E%3Crect x='56' y='56' width='4' height='4' fill='%23fef3c7' opacity='0.35'/%3E%3Crect x='60' y='56' width='4' height='4' fill='%23fde68a' opacity='0.3'/%3E%3Crect x='56' y='60' width='4' height='4' fill='%23fde68a' opacity='0.28'/%3E%3Crect x='60' y='60' width='4' height='4' fill='%23fef9c3' opacity='0.4'/%3E%3Crect x='52' y='56' width='4' height='4' fill='%23c4b5fd' opacity='0.2'/%3E%3Crect x='64' y='56' width='4' height='4' fill='%23a78bfa' opacity='0.18'/%3E%3Crect x='56' y='52' width='4' height='4' fill='%23818cf8' opacity='0.2'/%3E%3Crect x='60' y='64' width='4' height='4' fill='%23c4b5fd' opacity='0.18'/%3E%3C!-- spiral arm top-right --%3E%3Crect x='68' y='48' width='4' height='4' fill='%236366f1' opacity='0.18'/%3E%3Crect x='72' y='44' width='4' height='4' fill='%23818cf8' opacity='0.14'/%3E%3Crect x='76' y='40' width='4' height='4' fill='%236366f1' opacity='0.12'/%3E%3Crect x='80' y='38' width='4' height='2' fill='%23818cf8' opacity='0.1'/%3E%3Crect x='84' y='36' width='4' height='2' fill='%236366f1' opacity='0.08'/%3E%3Crect x='88' y='36' width='2' height='2' fill='%23a78bfa' opacity='0.06'/%3E%3C!-- spiral arm bottom-left --%3E%3Crect x='48' y='68' width='4' height='4' fill='%23818cf8' opacity='0.16'/%3E%3Crect x='44' y='72' width='4' height='4' fill='%236366f1' opacity='0.14'/%3E%3Crect x='40' y='76' width='4' height='4' fill='%23818cf8' opacity='0.12'/%3E%3Crect x='36' y='78' width='4' height='2' fill='%236366f1' opacity='0.1'/%3E%3Crect x='32' y='80' width='4' height='2' fill='%23a78bfa' opacity='0.07'/%3E%3C!-- spiral arm top-left --%3E%3Crect x='48' y='48' width='4' height='4' fill='%23c084fc' opacity='0.15'/%3E%3Crect x='44' y='44' width='4' height='4' fill='%23a855f7' opacity='0.12'/%3E%3Crect x='40' y='40' width='4' height='4' fill='%23c084fc' opacity='0.1'/%3E%3Crect x='36' y='38' width='4' height='2' fill='%23a855f7' opacity='0.08'/%3E%3C!-- spiral arm bottom-right --%3E%3Crect x='68' y='68' width='4' height='4' fill='%23a855f7' opacity='0.14'/%3E%3Crect x='72' y='72' width='4' height='4' fill='%23c084fc' opacity='0.12'/%3E%3Crect x='76' y='76' width='4' height='4' fill='%23a855f7' opacity='0.1'/%3E%3Crect x='80' y='78' width='4' height='2' fill='%23c084fc' opacity='0.07'/%3E%3C!-- nebula clouds --%3E%3Crect x='50' y='40' width='8' height='2' fill='%23f0abfc' opacity='0.07'/%3E%3Crect x='66' y='62' width='6' height='2' fill='%2367e8f9' opacity='0.06'/%3E%3Crect x='42' y='64' width='6' height='2' fill='%23f0abfc' opacity='0.05'/%3E%3Crect x='64' y='44' width='4' height='2' fill='%2367e8f9' opacity='0.06'/%3E%3C!-- scattered stars --%3E%3Crect x='20' y='18' width='2' height='2' fill='white' opacity='0.2'/%3E%3Crect x='95' y='22' width='2' height='2' fill='white' opacity='0.15'/%3E%3Crect x='14' y='90' width='2' height='2' fill='white' opacity='0.12'/%3E%3Crect x='100' y='88' width='2' height='2' fill='white' opacity='0.18'/%3E%3Crect x='30' y='105' width='2' height='2' fill='%23c4b5fd' opacity='0.1'/%3E%3Crect x='108' y='14' width='2' height='2' fill='%23fde68a' opacity='0.12'/%3E%3Crect x='10' y='50' width='2' height='2' fill='white' opacity='0.1'/%3E%3Crect x='110' y='60' width='2' height='2' fill='%2367e8f9' opacity='0.12'/%3E%3Crect x='70' y='20' width='2' height='2' fill='white' opacity='0.08'/%3E%3Crect x='50' y='100' width='2' height='2' fill='white' opacity='0.1'/%3E%3Crect x='85' y='105' width='2' height='2' fill='%23fde68a' opacity='0.08'/%3E%3Crect x='25' y='30' width='2' height='2' fill='%23c4b5fd' opacity='0.08'/%3E%3C/svg%3E") no-repeat center center / 100% 100%;
|
/* Subtle light rays from surface */
|
||||||
|
repeating-linear-gradient(
|
||||||
|
-25deg,
|
||||||
|
transparent 0px,
|
||||||
|
transparent 60px,
|
||||||
|
rgba(103, 232, 249, 0.018) 60px,
|
||||||
|
rgba(103, 232, 249, 0.018) 63px,
|
||||||
|
transparent 63px,
|
||||||
|
transparent 180px
|
||||||
|
),
|
||||||
|
/* Water depth gradient */
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(0, 8, 28, 0.96) 0%,
|
||||||
|
rgba(0, 18, 48, 0.94) 20%,
|
||||||
|
rgba(0, 30, 60, 0.92) 40%,
|
||||||
|
rgba(2, 45, 72, 0.88) 60%,
|
||||||
|
rgba(5, 55, 70, 0.85) 75%,
|
||||||
|
rgba(12, 58, 58, 0.82) 88%,
|
||||||
|
rgba(30, 55, 40, 0.78) 100%
|
||||||
|
),
|
||||||
|
/* Pixel art sea floor: sand, seaweed, coral, rocks, starfish, shell */
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='120' viewBox='0 0 240 120' shape-rendering='crispEdges'%3E%3Crect x='0' y='88' width='240' height='32' fill='%23c2a060' opacity='0.6'/%3E%3Crect x='0' y='86' width='240' height='4' fill='%23d4b878' opacity='0.5'/%3E%3Crect x='10' y='92' width='20' height='2' fill='%23a89048' opacity='0.3'/%3E%3Crect x='50' y='96' width='16' height='2' fill='%23b8a060' opacity='0.25'/%3E%3Crect x='90' y='94' width='24' height='2' fill='%23a89048' opacity='0.3'/%3E%3Crect x='140' y='98' width='20' height='2' fill='%23b8a060' opacity='0.25'/%3E%3Crect x='190' y='92' width='18' height='2' fill='%23a89048' opacity='0.28'/%3E%3Crect x='15' y='80' width='12' height='8' fill='%23475569' opacity='0.55'/%3E%3Crect x='17' y='78' width='8' height='4' fill='%2364748b' opacity='0.45'/%3E%3Crect x='19' y='76' width='4' height='4' fill='%23718096' opacity='0.35'/%3E%3Crect x='45' y='48' width='2' height='40' fill='%2316a34a' opacity='0.55'/%3E%3Crect x='47' y='44' width='2' height='12' fill='%2322c55e' opacity='0.5'/%3E%3Crect x='43' y='52' width='2' height='8' fill='%2315803d' opacity='0.45'/%3E%3Crect x='49' y='40' width='2' height='8' fill='%2322c55e' opacity='0.4'/%3E%3Crect x='41' y='56' width='2' height='6' fill='%2316a34a' opacity='0.35'/%3E%3Crect x='75' y='72' width='2' height='16' fill='%23f87171' opacity='0.5'/%3E%3Crect x='73' y='68' width='2' height='8' fill='%23fb7185' opacity='0.45'/%3E%3Crect x='77' y='70' width='2' height='6' fill='%23f472b6' opacity='0.4'/%3E%3Crect x='71' y='66' width='2' height='4' fill='%23f87171' opacity='0.35'/%3E%3Crect x='79' y='68' width='2' height='4' fill='%23fb7185' opacity='0.35'/%3E%3Crect x='69' y='68' width='2' height='2' fill='%23f472b6' opacity='0.3'/%3E%3Crect x='81' y='70' width='2' height='2' fill='%23f87171' opacity='0.3'/%3E%3Crect x='105' y='62' width='2' height='26' fill='%2322c55e' opacity='0.5'/%3E%3Crect x='107' y='58' width='2' height='10' fill='%2316a34a' opacity='0.45'/%3E%3Crect x='103' y='66' width='2' height='6' fill='%2315803d' opacity='0.4'/%3E%3Crect x='125' y='86' width='2' height='8' fill='%23f97316' opacity='0.5'/%3E%3Crect x='121' y='88' width='10' height='2' fill='%23f97316' opacity='0.5'/%3E%3Crect x='123' y='84' width='2' height='2' fill='%23fb923c' opacity='0.4'/%3E%3Crect x='129' y='84' width='2' height='2' fill='%23fb923c' opacity='0.4'/%3E%3Crect x='121' y='92' width='2' height='2' fill='%23fb923c' opacity='0.4'/%3E%3Crect x='131' y='92' width='2' height='2' fill='%23fb923c' opacity='0.4'/%3E%3Crect x='150' y='50' width='2' height='38' fill='%2316a34a' opacity='0.5'/%3E%3Crect x='152' y='46' width='2' height='10' fill='%2322c55e' opacity='0.45'/%3E%3Crect x='148' y='54' width='2' height='8' fill='%2315803d' opacity='0.4'/%3E%3Crect x='154' y='42' width='2' height='8' fill='%2322c55e' opacity='0.35'/%3E%3Crect x='180' y='76' width='2' height='12' fill='%23818cf8' opacity='0.45'/%3E%3Crect x='178' y='72' width='2' height='6' fill='%23a78bfa' opacity='0.4'/%3E%3Crect x='182' y='74' width='2' height='4' fill='%23c084fc' opacity='0.35'/%3E%3Crect x='176' y='74' width='2' height='2' fill='%23818cf8' opacity='0.3'/%3E%3Crect x='184' y='76' width='2' height='2' fill='%23a78bfa' opacity='0.3'/%3E%3Crect x='210' y='86' width='6' height='4' fill='%23fef3c7' opacity='0.4'/%3E%3Crect x='212' y='84' width='4' height='2' fill='%23fde68a' opacity='0.35'/%3E%3Crect x='214' y='82' width='2' height='2' fill='%23fef3c7' opacity='0.3'/%3E%3Crect x='220' y='82' width='10' height='6' fill='%23475569' opacity='0.5'/%3E%3Crect x='222' y='80' width='6' height='4' fill='%2364748b' opacity='0.4'/%3E%3Crect x='35' y='90' width='2' height='2' fill='%2364748b' opacity='0.3'/%3E%3Crect x='60' y='92' width='2' height='2' fill='%2364748b' opacity='0.25'/%3E%3Crect x='95' y='90' width='2' height='2' fill='%23475569' opacity='0.3'/%3E%3Crect x='160' y='92' width='2' height='2' fill='%2364748b' opacity='0.25'/%3E%3Crect x='200' y='90' width='2' height='2' fill='%23475569' opacity='0.28'/%3E%3C/svg%3E") repeat-x bottom center / 240px 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated bubbles rising through the water */
|
||||||
|
.content::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
background:
|
||||||
|
/* Bubble layer 1 - larger, slower */
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='200' viewBox='0 0 80 200' shape-rendering='crispEdges'%3E%3Crect x='20' y='30' width='4' height='4' fill='%2367e8f9' opacity='0.22'/%3E%3Crect x='60' y='90' width='4' height='4' fill='%2367e8f9' opacity='0.18'/%3E%3Crect x='35' y='150' width='4' height='4' fill='%2367e8f9' opacity='0.24'/%3E%3Crect x='50' y='20' width='2' height='2' fill='white' opacity='0.16'/%3E%3Crect x='10' y='70' width='2' height='2' fill='white' opacity='0.13'/%3E%3Crect x='70' y='130' width='2' height='2' fill='white' opacity='0.16'/%3E%3Crect x='25' y='180' width='2' height='2' fill='white' opacity='0.13'/%3E%3Crect x='40' y='50' width='2' height='2' fill='%2367e8f9' opacity='0.1'/%3E%3Crect x='5' y='110' width='2' height='2' fill='%2367e8f9' opacity='0.09'/%3E%3Crect x='55' y='170' width='2' height='2' fill='white' opacity='0.11'/%3E%3C/svg%3E") repeat / 80px 200px,
|
||||||
|
/* Bubble layer 2 - smaller, different rhythm */
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='280' viewBox='0 0 100 280' shape-rendering='crispEdges'%3E%3Crect x='30' y='40' width='4' height='4' fill='%2367e8f9' opacity='0.16'/%3E%3Crect x='75' y='110' width='2' height='2' fill='white' opacity='0.13'/%3E%3Crect x='15' y='180' width='4' height='4' fill='%2367e8f9' opacity='0.2'/%3E%3Crect x='55' y='240' width='2' height='2' fill='white' opacity='0.11'/%3E%3Crect x='85' y='60' width='2' height='2' fill='%2367e8f9' opacity='0.1'/%3E%3Crect x='45' y='150' width='2' height='2' fill='white' opacity='0.13'/%3E%3Crect x='10' y='260' width='2' height='2' fill='%2367e8f9' opacity='0.09'/%3E%3Crect x='65' y='20' width='2' height='2' fill='white' opacity='0.11'/%3E%3C/svg%3E") repeat / 100px 280px;
|
||||||
|
animation: sea-bubbles 14s linear infinite, water-sway 8s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated pixel art fish swimming across */
|
||||||
|
.content::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
background:
|
||||||
|
/* Fish 1: orange tropical fish (right-facing, swims left→right) */
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='10' viewBox='0 0 16 10' shape-rendering='crispEdges'%3E%3Crect x='0' y='1' width='2' height='2' fill='%23f97316' opacity='0.6'/%3E%3Crect x='0' y='7' width='2' height='2' fill='%23f97316' opacity='0.6'/%3E%3Crect x='2' y='2' width='2' height='6' fill='%23fb923c' opacity='0.6'/%3E%3Crect x='4' y='1' width='8' height='8' fill='%23f97316' opacity='0.55'/%3E%3Crect x='7' y='1' width='2' height='8' fill='%23fef3c7' opacity='0.4'/%3E%3Crect x='5' y='0' width='4' height='1' fill='%23fb923c' opacity='0.4'/%3E%3Crect x='5' y='9' width='4' height='1' fill='%23fb923c' opacity='0.4'/%3E%3Crect x='10' y='3' width='2' height='2' fill='%23000' opacity='0.5'/%3E%3Crect x='11' y='3' width='1' height='1' fill='white' opacity='0.4'/%3E%3Crect x='12' y='5' width='2' height='1' fill='%23000' opacity='0.3'/%3E%3C/svg%3E") no-repeat / 32px 20px,
|
||||||
|
/* Fish 2: blue fish (left-facing, swims right→left) */
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' shape-rendering='crispEdges'%3E%3Crect x='10' y='1' width='2' height='2' fill='%236366f1' opacity='0.5'/%3E%3Crect x='10' y='5' width='2' height='2' fill='%236366f1' opacity='0.5'/%3E%3Crect x='8' y='2' width='2' height='4' fill='%23818cf8' opacity='0.5'/%3E%3Crect x='2' y='1' width='6' height='6' fill='%236366f1' opacity='0.45'/%3E%3Crect x='2' y='2' width='2' height='2' fill='%23000' opacity='0.4'/%3E%3Crect x='2' y='2' width='1' height='1' fill='white' opacity='0.3'/%3E%3Crect x='4' y='0' width='3' height='1' fill='%23818cf8' opacity='0.35'/%3E%3C/svg%3E") no-repeat / 24px 16px;
|
||||||
|
animation: fish-swim 20s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sea-bubbles {
|
||||||
|
from { background-position: 0 0, 40px 0; }
|
||||||
|
to { background-position: 0 -200px, 0 -280px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes water-sway {
|
||||||
|
from { transform: translateX(-3px); }
|
||||||
|
to { transform: translateX(3px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fish-swim {
|
||||||
|
0% { background-position: -40px 25%, calc(100% + 30px) 55%; }
|
||||||
|
100% { background-position: calc(100% + 40px) 30%, -30px 50%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override ChatContainer backgrounds for black transparency */
|
/* Override ChatContainer backgrounds for black transparency */
|
||||||
|
|||||||
@@ -103,12 +103,9 @@ function formatTokens(n?: number): string {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.assistant-bubble {
|
.assistant-bubble {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 12px;
|
border-radius: 0;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.5rem 0.25rem;
|
||||||
margin-right: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-bubble.continuation {
|
.assistant-bubble.continuation {
|
||||||
|
|||||||
@@ -107,6 +107,51 @@ async function copySelected() {
|
|||||||
setTimeout(() => (selectedCopied.value = false), 1500)
|
setTimeout(() => (selectedCopied.value = false), 1500)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Collapse sections ──
|
||||||
|
const collapsedSections = ref(new Set<string>())
|
||||||
|
|
||||||
|
// For each message, find which user message "owns" it (section leader)
|
||||||
|
const sectionMap = computed(() => {
|
||||||
|
const map = new Map<string, string>() // messageUuid → ownerUserUuid
|
||||||
|
let currentUserUuid: string | null = null
|
||||||
|
for (const msg of props.conversation.messages) {
|
||||||
|
if (msg.kind === 'user') {
|
||||||
|
currentUserUuid = msg.uuid
|
||||||
|
} else if (currentUserUuid) {
|
||||||
|
map.set(msg.uuid, currentUserUuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
// Count of non-user messages per section
|
||||||
|
const sectionCounts = computed(() => {
|
||||||
|
const counts = new Map<string, number>()
|
||||||
|
let currentUserUuid: string | null = null
|
||||||
|
for (const msg of props.conversation.messages) {
|
||||||
|
if (msg.kind === 'user') {
|
||||||
|
currentUserUuid = msg.uuid
|
||||||
|
if (!counts.has(currentUserUuid)) counts.set(currentUserUuid, 0)
|
||||||
|
} else if (currentUserUuid) {
|
||||||
|
counts.set(currentUserUuid, (counts.get(currentUserUuid) || 0) + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return counts
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleCollapse(userUuid: string) {
|
||||||
|
const s = new Set(collapsedSections.value)
|
||||||
|
if (s.has(userUuid)) s.delete(userUuid)
|
||||||
|
else s.add(userUuid)
|
||||||
|
collapsedSections.value = s
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCollapsedChild(msg: { uuid: string; kind: string }): boolean {
|
||||||
|
if (msg.kind === 'user') return false
|
||||||
|
const owner = sectionMap.value.get(msg.uuid)
|
||||||
|
return !!owner && collapsedSections.value.has(owner)
|
||||||
|
}
|
||||||
|
|
||||||
// Track messages that just resolved from optimistic → real
|
// Track messages that just resolved from optimistic → real
|
||||||
// These skip the bounce animation and get a smooth transition instead
|
// These skip the bounce animation and get a smooth transition instead
|
||||||
const resolvedUuids = ref(new Set<string>())
|
const resolvedUuids = ref(new Set<string>())
|
||||||
@@ -207,13 +252,16 @@ function formatDuration(start: string, end: string): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="scrollContainer" class="messages-scroll">
|
<div ref="scrollContainer" class="messages-scroll">
|
||||||
<div
|
<template
|
||||||
v-for="(msg, idx) in conversation.messages"
|
v-for="(msg, idx) in conversation.messages"
|
||||||
:key="msg.uuid"
|
:key="msg.uuid"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!isCollapsedChild(msg)"
|
||||||
:class="['message-wrapper', {
|
:class="['message-wrapper', {
|
||||||
resolved: resolvedUuids.has(msg.uuid),
|
resolved: resolvedUuids.has(msg.uuid),
|
||||||
selected: selectedUuids.has(msg.uuid),
|
selected: selectedUuids.has(msg.uuid),
|
||||||
'assistant-continuation': msg.kind === 'assistant' && idx > 0 && conversation.messages[idx - 1].kind === 'assistant'
|
'assistant-continuation': msg.kind === 'assistant' && idx > 0 && conversation.messages[idx - 1].kind === 'assistant' && !isCollapsedChild(conversation.messages[idx - 1])
|
||||||
}]"
|
}]"
|
||||||
>
|
>
|
||||||
<!-- Select checkbox -->
|
<!-- Select checkbox -->
|
||||||
@@ -232,6 +280,9 @@ function formatDuration(start: string, end: string): string {
|
|||||||
<UserMessageBubble
|
<UserMessageBubble
|
||||||
v-if="msg.kind === 'user'"
|
v-if="msg.kind === 'user'"
|
||||||
:message="msg"
|
:message="msg"
|
||||||
|
:collapsed="collapsedSections.has(msg.uuid)"
|
||||||
|
:section-count="sectionCounts.get(msg.uuid) || 0"
|
||||||
|
@toggle-collapse="toggleCollapse(msg.uuid)"
|
||||||
/>
|
/>
|
||||||
<AssistantMessageBubble
|
<AssistantMessageBubble
|
||||||
v-else-if="msg.kind === 'assistant'"
|
v-else-if="msg.kind === 'assistant'"
|
||||||
@@ -248,6 +299,7 @@ function formatDuration(start: string, end: string): string {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Selection floating bar -->
|
<!-- Selection floating bar -->
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ import type { ParsedUserMessage } from '@/types/transcript-debug'
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
message: ParsedUserMessage
|
message: ParsedUserMessage
|
||||||
|
collapsed?: boolean
|
||||||
|
sectionCount?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
toggleCollapse: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isOptimistic = computed(() => props.message.uuid.startsWith('optimistic-'))
|
const isOptimistic = computed(() => props.message.uuid.startsWith('optimistic-'))
|
||||||
@@ -15,8 +21,10 @@ function formatTime(ts: string): string {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="['user-bubble', { meta: message.isMeta, optimistic: isOptimistic }]">
|
<div :class="['user-divider', { meta: message.isMeta, optimistic: isOptimistic }]">
|
||||||
<div class="bubble-header">
|
<div class="divider-line" />
|
||||||
|
<div class="divider-content">
|
||||||
|
<div class="divider-header">
|
||||||
<span class="role-badge">User</span>
|
<span class="role-badge">User</span>
|
||||||
<span v-if="message.isMeta" class="meta-badge">meta</span>
|
<span v-if="message.isMeta" class="meta-badge">meta</span>
|
||||||
<span v-if="isOptimistic" class="sending-badge">
|
<span v-if="isOptimistic" class="sending-badge">
|
||||||
@@ -25,47 +33,139 @@ function formatTime(ts: string): string {
|
|||||||
<span class="sending-dot"></span>
|
<span class="sending-dot"></span>
|
||||||
Sending
|
Sending
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="sectionCount && sectionCount > 0"
|
||||||
|
:class="['collapse-btn', { collapsed }]"
|
||||||
|
@click.stop="emit('toggleCollapse')"
|
||||||
|
:title="collapsed ? 'Expand section' : 'Collapse section'"
|
||||||
|
>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline :points="collapsed ? '9 18 15 12 9 6' : '6 9 12 15 18 9'" />
|
||||||
|
</svg>
|
||||||
|
<span v-if="collapsed" class="collapse-count">{{ sectionCount }}</span>
|
||||||
|
</button>
|
||||||
<span class="timestamp">{{ formatTime(message.timestamp) }}</span>
|
<span class="timestamp">{{ formatTime(message.timestamp) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bubble-content">{{ message.content }}</div>
|
<div class="divider-text">{{ message.content }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.user-bubble {
|
.user-divider {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.75rem 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-line {
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(129, 140, 248, 0.2) 10%, rgba(129, 140, 248, 0.2) 90%, transparent);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-content {
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #818cf8;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge {
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 0.05rem 0.3rem;
|
||||||
|
border-radius: 3px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
backdrop-filter: blur(12px);
|
color: rgba(251, 191, 36, 0.7);
|
||||||
-webkit-backdrop-filter: blur(12px);
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-left: auto;
|
||||||
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-text {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Meta messages: dimmed */
|
||||||
|
.user-divider.meta {
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-divider.meta .divider-line {
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(251, 191, 36, 0.15) 10%, rgba(251, 191, 36, 0.15) 90%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapse button */
|
||||||
|
.collapse-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 1px 4px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 12px;
|
border-radius: 3px;
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
margin-left: 2rem;
|
|
||||||
transition: opacity 0.3s, border-color 0.3s, background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-bubble.meta {
|
|
||||||
opacity: 0.5;
|
|
||||||
border-style: dashed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optimistic / sending state */
|
|
||||||
.user-bubble.optimistic {
|
|
||||||
opacity: 0.7;
|
|
||||||
border-color: rgba(99, 102, 241, 0.08);
|
|
||||||
border-style: dashed;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-btn:hover {
|
||||||
|
background: rgba(129, 140, 248, 0.1);
|
||||||
|
color: #818cf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-btn svg {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-count {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-btn:hover .collapse-count {
|
||||||
|
color: #818cf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimistic / sending */
|
||||||
|
.user-divider.optimistic {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-divider.optimistic .divider-line {
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(99, 102, 241, 0.1) 10%, rgba(99, 102, 241, 0.1) 90%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sending-badge {
|
.sending-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
font-size: 10px;
|
font-size: 9px;
|
||||||
color: var(--accent, #6366f1);
|
color: var(--accent, #6366f1);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 0.1rem 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: rgba(99, 102, 241, 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sending-dot {
|
.sending-dot {
|
||||||
@@ -83,42 +183,4 @@ function formatTime(ts: string): string {
|
|||||||
0%, 80%, 100% { opacity: 0.2; }
|
0%, 80%, 100% { opacity: 0.2; }
|
||||||
40% { opacity: 1; }
|
40% { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.bubble-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-badge {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #818cf8;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge {
|
|
||||||
font-size: 10px;
|
|
||||||
padding: 0.1rem 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: rgba(251, 191, 36, 0.15);
|
|
||||||
color: #fbbf24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timestamp {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-left: auto;
|
|
||||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-content {
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--text-primary);
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ const isError = computed(() => props.call.result?.isError ?? false)
|
|||||||
|
|
||||||
const highlightedCommand = computed(() => highlightCode(command.value, 'bash'))
|
const highlightedCommand = computed(() => highlightCode(command.value, 'bash'))
|
||||||
|
|
||||||
const cmdExpanded = ref(false)
|
const expanded = ref(false)
|
||||||
const resultExpanded = ref(false)
|
|
||||||
|
|
||||||
const cmdPreview = computed(() => {
|
const cmdPreview = computed(() => {
|
||||||
const c = command.value.replace(/\n/g, ' ').trim()
|
const c = command.value.replace(/\n/g, ' ').trim()
|
||||||
@@ -28,7 +27,7 @@ const cmdPreview = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="['bash-card', { error: isError }]">
|
<div :class="['bash-card', { error: isError }]">
|
||||||
<div class="card-header">
|
<div class="card-header" @click.stop="expanded = !expanded">
|
||||||
<span class="card-icon">
|
<span class="card-icon">
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<polyline points="4 17 10 11 4 5"/>
|
<polyline points="4 17 10 11 4 5"/>
|
||||||
@@ -40,32 +39,18 @@ const cmdPreview = computed(() => {
|
|||||||
<span v-if="runInBackground" class="info-badge">bg</span>
|
<span v-if="runInBackground" class="info-badge">bg</span>
|
||||||
<span v-if="timeout" class="info-badge">{{ timeout }}ms</span>
|
<span v-if="timeout" class="info-badge">{{ timeout }}ms</span>
|
||||||
<span v-if="isError" class="error-badge">err</span>
|
<span v-if="isError" class="error-badge">err</span>
|
||||||
|
|
||||||
<span class="header-spacer"></span>
|
|
||||||
|
|
||||||
<button class="toggle-btn" :class="{ active: cmdExpanded }" @click.stop="cmdExpanded = !cmdExpanded" title="Toggle command">
|
|
||||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M12 3v18M3 12h18"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button v-if="call.result" class="toggle-btn" :class="{ active: resultExpanded }" @click.stop="resultExpanded = !resultExpanded" title="Toggle result">
|
|
||||||
<svg width="11" height="11" 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>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Full command -->
|
<template v-if="expanded">
|
||||||
<div v-if="cmdExpanded" class="command-section">
|
<div class="command-section">
|
||||||
<div v-if="description" class="description">{{ description }}</div>
|
<div v-if="description" class="description">{{ description }}</div>
|
||||||
<div class="command-row">
|
<div class="command-row">
|
||||||
<span class="prompt">$</span>
|
<span class="prompt">$</span>
|
||||||
<pre class="command-text" v-html="highlightedCommand"></pre>
|
<pre class="command-text" v-html="highlightedCommand"></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ToolResultBlock v-if="call.result" :result="call.result" />
|
||||||
<ToolResultBlock v-if="resultExpanded && call.result" :result="call.result" />
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -88,7 +73,9 @@ const cmdPreview = computed(() => {
|
|||||||
padding: 0.3rem 0.6rem;
|
padding: 0.3rem 0.6rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.card-header:hover { background: rgba(245, 158, 11, 0.04); }
|
||||||
|
|
||||||
.card-icon { display: flex; align-items: center; color: rgba(245, 158, 11, 0.6); flex-shrink: 0; }
|
.card-icon { display: flex; align-items: center; color: rgba(245, 158, 11, 0.6); flex-shrink: 0; }
|
||||||
.bash-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
.bash-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ const ext = computed(() => {
|
|||||||
|
|
||||||
const isError = computed(() => props.call.result?.isError ?? false)
|
const isError = computed(() => props.call.result?.isError ?? false)
|
||||||
|
|
||||||
const diffExpanded = ref(false)
|
const expanded = ref(false)
|
||||||
const resultExpanded = ref(false)
|
|
||||||
|
|
||||||
const highlightedOld = computed(() => highlightCode(oldString.value, ext.value || undefined))
|
const highlightedOld = computed(() => highlightCode(oldString.value, ext.value || undefined))
|
||||||
const highlightedNew = computed(() => highlightCode(newString.value, ext.value || undefined))
|
const highlightedNew = computed(() => highlightCode(newString.value, ext.value || undefined))
|
||||||
@@ -39,7 +38,7 @@ const newLineCount = computed(() => newString.value.split('\n').length)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="['edit-card', { error: isError }]">
|
<div :class="['edit-card', { error: isError }]">
|
||||||
<div class="card-header">
|
<div class="card-header" @click.stop="expanded = !expanded">
|
||||||
<span class="card-icon">
|
<span class="card-icon">
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="12" height="12" 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="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||||
@@ -53,24 +52,11 @@ const newLineCount = computed(() => newString.value.split('\n').length)
|
|||||||
<span class="diff-removed">-{{ oldLineCount }}</span>
|
<span class="diff-removed">-{{ oldLineCount }}</span>
|
||||||
<span class="diff-added">+{{ newLineCount }}</span>
|
<span class="diff-added">+{{ newLineCount }}</span>
|
||||||
<span v-if="isError" class="error-badge">err</span>
|
<span v-if="isError" class="error-badge">err</span>
|
||||||
|
|
||||||
<span class="header-spacer"></span>
|
|
||||||
|
|
||||||
<button class="toggle-btn" :class="{ active: diffExpanded }" @click.stop="diffExpanded = !diffExpanded" title="Toggle diff">
|
|
||||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M12 3v18M3 12h18"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button v-if="call.result" class="toggle-btn" :class="{ active: resultExpanded }" @click.stop="resultExpanded = !resultExpanded" title="Toggle result">
|
|
||||||
<svg width="11" height="11" 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>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-if="expanded">
|
||||||
<!-- Diff view -->
|
<!-- Diff view -->
|
||||||
<div v-if="diffExpanded" class="diff-content">
|
<div class="diff-content">
|
||||||
<div class="diff-block removed">
|
<div class="diff-block removed">
|
||||||
<div class="diff-label"><span class="diff-sign">-</span> old</div>
|
<div class="diff-label"><span class="diff-sign">-</span> old</div>
|
||||||
<pre class="diff-code" v-html="highlightedOld"></pre>
|
<pre class="diff-code" v-html="highlightedOld"></pre>
|
||||||
@@ -82,7 +68,8 @@ const newLineCount = computed(() => newString.value.split('\n').length)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Result -->
|
<!-- Result -->
|
||||||
<ToolResultBlock v-if="resultExpanded && call.result" :result="call.result" />
|
<ToolResultBlock v-if="call.result" :result="call.result" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -105,8 +92,11 @@ const newLineCount = computed(() => newString.value.split('\n').length)
|
|||||||
padding: 0.3rem 0.6rem;
|
padding: 0.3rem 0.6rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-header:hover { background: rgba(99, 102, 241, 0.04); }
|
||||||
|
|
||||||
.card-icon { display: flex; align-items: center; color: rgba(99, 102, 241, 0.6); flex-shrink: 0; }
|
.card-icon { display: flex; align-items: center; color: rgba(99, 102, 241, 0.6); flex-shrink: 0; }
|
||||||
.edit-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
.edit-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
||||||
|
|
||||||
@@ -147,26 +137,6 @@ const newLineCount = computed(() => newString.value.split('\n').length)
|
|||||||
.diff-added { color: rgba(34, 197, 94, 0.7); }
|
.diff-added { color: rgba(34, 197, 94, 0.7); }
|
||||||
.error-badge { color: rgba(239, 68, 68, 0.7); }
|
.error-badge { color: rgba(239, 68, 68, 0.7); }
|
||||||
|
|
||||||
.header-spacer { flex: 1; }
|
|
||||||
|
|
||||||
.toggle-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-muted);
|
|
||||||
cursor: pointer;
|
|
||||||
flex-shrink: 0;
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
.toggle-btn:hover { opacity: 0.8; }
|
|
||||||
.toggle-btn.active { opacity: 1; color: rgba(99, 102, 241, 0.8); }
|
|
||||||
|
|
||||||
/* Diff */
|
/* Diff */
|
||||||
.diff-content { border-top: 1px solid rgba(255, 255, 255, 0.04); }
|
.diff-content { border-top: 1px solid rgba(255, 255, 255, 0.04); }
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const fileCount = computed(() => {
|
|||||||
return content.split('\n').filter(l => l.trim()).length
|
return content.split('\n').filter(l => l.trim()).length
|
||||||
})
|
})
|
||||||
|
|
||||||
const resultExpanded = ref(false)
|
const expanded = ref(false)
|
||||||
|
|
||||||
const shortPath = computed(() => {
|
const shortPath = computed(() => {
|
||||||
if (!path.value) return ''
|
if (!path.value) return ''
|
||||||
@@ -29,7 +29,7 @@ const shortPath = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="['glob-card', { error: isError }]">
|
<div :class="['glob-card', { error: isError }]">
|
||||||
<div class="card-header">
|
<div class="card-header" @click.stop="expanded = !expanded">
|
||||||
<span class="card-icon">
|
<span class="card-icon">
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="12" height="12" 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"/>
|
<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"/>
|
||||||
@@ -40,18 +40,9 @@ const shortPath = computed(() => {
|
|||||||
<span v-if="path" class="scope-badge" :title="path">{{ shortPath }}</span>
|
<span v-if="path" class="scope-badge" :title="path">{{ shortPath }}</span>
|
||||||
<span v-if="fileCount != null && !isError" class="match-count">{{ fileCount }}</span>
|
<span v-if="fileCount != null && !isError" class="match-count">{{ fileCount }}</span>
|
||||||
<span v-if="isError" class="error-badge">err</span>
|
<span v-if="isError" class="error-badge">err</span>
|
||||||
|
|
||||||
<span class="header-spacer"></span>
|
|
||||||
|
|
||||||
<button v-if="call.result" class="toggle-btn" :class="{ active: resultExpanded }" @click.stop="resultExpanded = !resultExpanded" title="Toggle result">
|
|
||||||
<svg width="11" height="11" 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>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ToolResultBlock v-if="resultExpanded && call.result" :result="call.result" />
|
<ToolResultBlock v-if="expanded && call.result" :result="call.result" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -74,7 +65,9 @@ const shortPath = computed(() => {
|
|||||||
padding: 0.3rem 0.6rem;
|
padding: 0.3rem 0.6rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.card-header:hover { background: rgba(251, 191, 36, 0.04); }
|
||||||
|
|
||||||
.card-icon { display: flex; align-items: center; color: rgba(251, 191, 36, 0.6); flex-shrink: 0; }
|
.card-icon { display: flex; align-items: center; color: rgba(251, 191, 36, 0.6); flex-shrink: 0; }
|
||||||
.glob-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
.glob-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ const flags = computed(() => {
|
|||||||
return f
|
return f
|
||||||
})
|
})
|
||||||
|
|
||||||
const detailsExpanded = ref(false)
|
const expanded = ref(false)
|
||||||
const resultExpanded = ref(false)
|
|
||||||
|
|
||||||
const shortPath = computed(() => {
|
const shortPath = computed(() => {
|
||||||
if (!path.value) return ''
|
if (!path.value) return ''
|
||||||
@@ -53,7 +52,7 @@ const shortPath = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="['grep-card', { error: isError }]">
|
<div :class="['grep-card', { error: isError }]">
|
||||||
<div class="card-header">
|
<div class="card-header" @click.stop="expanded = !expanded">
|
||||||
<span class="card-icon">
|
<span class="card-icon">
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="11" cy="11" r="8"/>
|
<circle cx="11" cy="11" r="8"/>
|
||||||
@@ -66,31 +65,17 @@ const shortPath = computed(() => {
|
|||||||
<span v-if="type" class="scope-badge type">{{ type }}</span>
|
<span v-if="type" class="scope-badge type">{{ type }}</span>
|
||||||
<span v-if="matchCount != null && !isError" class="match-count">{{ matchCount }}</span>
|
<span v-if="matchCount != null && !isError" class="match-count">{{ matchCount }}</span>
|
||||||
<span v-if="isError" class="error-badge">err</span>
|
<span v-if="isError" class="error-badge">err</span>
|
||||||
|
|
||||||
<span class="header-spacer"></span>
|
|
||||||
|
|
||||||
<button v-if="path || context || headLimit || outputMode !== 'files_with_matches'" class="toggle-btn" :class="{ active: detailsExpanded }" @click.stop="detailsExpanded = !detailsExpanded" title="Toggle details">
|
|
||||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button v-if="call.result" class="toggle-btn" :class="{ active: resultExpanded }" @click.stop="resultExpanded = !resultExpanded" title="Toggle result">
|
|
||||||
<svg width="11" height="11" 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>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Details row -->
|
<template v-if="expanded">
|
||||||
<div v-if="detailsExpanded" class="details-row">
|
<div v-if="path || context || headLimit || outputMode !== 'files_with_matches'" class="details-row">
|
||||||
<span v-if="path" class="detail-badge path" :title="path">{{ shortPath }}</span>
|
<span v-if="path" class="detail-badge path" :title="path">{{ shortPath }}</span>
|
||||||
<span v-if="outputMode !== 'files_with_matches'" class="detail-badge">{{ outputMode }}</span>
|
<span v-if="outputMode !== 'files_with_matches'" class="detail-badge">{{ outputMode }}</span>
|
||||||
<span v-if="context" class="detail-badge">{{ context }}</span>
|
<span v-if="context" class="detail-badge">{{ context }}</span>
|
||||||
<span v-if="headLimit" class="detail-badge">head {{ headLimit }}</span>
|
<span v-if="headLimit" class="detail-badge">head {{ headLimit }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<ToolResultBlock v-if="call.result" :result="call.result" />
|
||||||
<ToolResultBlock v-if="resultExpanded && call.result" :result="call.result" />
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -113,7 +98,9 @@ const shortPath = computed(() => {
|
|||||||
padding: 0.3rem 0.6rem;
|
padding: 0.3rem 0.6rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.card-header:hover { background: rgba(236, 72, 153, 0.04); }
|
||||||
|
|
||||||
.card-icon { display: flex; align-items: center; color: rgba(236, 72, 153, 0.6); flex-shrink: 0; }
|
.card-icon { display: flex; align-items: center; color: rgba(236, 72, 153, 0.6); flex-shrink: 0; }
|
||||||
.grep-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
.grep-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ const ext = computed(() => {
|
|||||||
return dot >= 0 ? name.slice(dot + 1).toLowerCase() : ''
|
return dot >= 0 ? name.slice(dot + 1).toLowerCase() : ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const resultExpanded = ref(false)
|
const expanded = ref(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="['read-card', { error: isError }]">
|
<div :class="['read-card', { error: isError }]">
|
||||||
<div class="card-header">
|
<div class="card-header" @click.stop="expanded = !expanded">
|
||||||
<span class="card-icon">
|
<span class="card-icon">
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="12" height="12" 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"/>
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
@@ -45,18 +45,9 @@ const resultExpanded = ref(false)
|
|||||||
<span v-if="limit != null" class="info-badge">{{ limit }}L</span>
|
<span v-if="limit != null" class="info-badge">{{ limit }}L</span>
|
||||||
<span v-if="pages" class="info-badge">p{{ pages }}</span>
|
<span v-if="pages" class="info-badge">p{{ pages }}</span>
|
||||||
<span v-if="isError" class="error-badge">err</span>
|
<span v-if="isError" class="error-badge">err</span>
|
||||||
|
|
||||||
<span class="header-spacer"></span>
|
|
||||||
|
|
||||||
<button v-if="call.result" class="toggle-btn" :class="{ active: resultExpanded }" @click.stop="resultExpanded = !resultExpanded" title="Toggle result">
|
|
||||||
<svg width="11" height="11" 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>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ToolResultBlock v-if="resultExpanded && call.result" :result="call.result" />
|
<ToolResultBlock v-if="expanded && call.result" :result="call.result" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -79,7 +70,9 @@ const resultExpanded = ref(false)
|
|||||||
padding: 0.3rem 0.6rem;
|
padding: 0.3rem 0.6rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.card-header:hover { background: rgba(6, 182, 212, 0.04); }
|
||||||
|
|
||||||
.card-icon { display: flex; align-items: center; color: rgba(6, 182, 212, 0.6); flex-shrink: 0; }
|
.card-icon { display: flex; align-items: center; color: rgba(6, 182, 212, 0.6); flex-shrink: 0; }
|
||||||
.read-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
.read-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ const subagentType = computed(() => (props.call.input?.subagent_type as string)
|
|||||||
const model = computed(() => (props.call.input?.model as string) || '')
|
const model = computed(() => (props.call.input?.model as string) || '')
|
||||||
const runInBackground = computed(() => props.call.input?.run_in_background as boolean | undefined)
|
const runInBackground = computed(() => props.call.input?.run_in_background as boolean | undefined)
|
||||||
|
|
||||||
// Toggles
|
const expanded = ref(false)
|
||||||
const showPrompt = ref(false)
|
|
||||||
const showResult = ref(false)
|
|
||||||
const showDesc = ref(false)
|
|
||||||
|
|
||||||
// ---- Extract main topics from result ----
|
// ---- Extract main topics from result ----
|
||||||
interface TopicItem {
|
interface TopicItem {
|
||||||
@@ -124,9 +121,9 @@ const cardColor = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasDesc = computed(() => description.value.length > 0)
|
const hasExpandable = computed(() =>
|
||||||
const hasPrompt = computed(() => prompt.value.length > 0)
|
description.value.length > 0 || prompt.value.length > 0 || !!props.call.result
|
||||||
const hasResult = computed(() => !!props.call.result)
|
)
|
||||||
|
|
||||||
// Brief for body (only when no topics to show)
|
// Brief for body (only when no topics to show)
|
||||||
const descBrief = computed(() => {
|
const descBrief = computed(() => {
|
||||||
@@ -143,7 +140,7 @@ const hasBody = computed(() =>
|
|||||||
<template>
|
<template>
|
||||||
<div :class="['task-card', { error: isError }]" :style="{ '--card-color': cardColor }">
|
<div :class="['task-card', { error: isError }]" :style="{ '--card-color': cardColor }">
|
||||||
<!-- HEADER -->
|
<!-- HEADER -->
|
||||||
<div class="card-header">
|
<div class="card-header" @click.stop="expanded = !expanded">
|
||||||
<span class="card-icon">
|
<span class="card-icon">
|
||||||
<svg v-if="toolName === 'Task'" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg v-if="toolName === 'Task'" width="13" height="13" 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"/>
|
<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"/>
|
||||||
@@ -171,25 +168,6 @@ const hasBody = computed(() =>
|
|||||||
<span v-if="model" class="model-badge">{{ model }}</span>
|
<span v-if="model" class="model-badge">{{ model }}</span>
|
||||||
<span v-if="runInBackground" class="bg-badge">bg</span>
|
<span v-if="runInBackground" class="bg-badge">bg</span>
|
||||||
<span v-if="isError" class="error-badge">err</span>
|
<span v-if="isError" class="error-badge">err</span>
|
||||||
|
|
||||||
<span class="header-spacer" />
|
|
||||||
|
|
||||||
<!-- Toggle buttons -->
|
|
||||||
<button v-if="hasDesc" class="toggle-btn" :class="{ active: showDesc }" @click.stop="showDesc = !showDesc" title="Description">
|
|
||||||
<svg width="11" height="11" 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>
|
|
||||||
</button>
|
|
||||||
<button v-if="hasPrompt" class="toggle-btn" :class="{ active: showPrompt }" @click.stop="showPrompt = !showPrompt" title="Prompt">
|
|
||||||
<svg width="11" height="11" 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>
|
|
||||||
</button>
|
|
||||||
<button v-if="hasResult" class="toggle-btn" :class="{ active: showResult }" @click.stop="showResult = !showResult" title="Result">
|
|
||||||
<svg width="11" height="11" 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>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- BODY: subject / brief -->
|
<!-- BODY: subject / brief -->
|
||||||
@@ -208,20 +186,18 @@ const hasBody = computed(() =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- EXPANDED: Description -->
|
<!-- EXPANDED: all details -->
|
||||||
<div v-if="showDesc && description" class="expand-section">
|
<template v-if="expanded">
|
||||||
|
<div v-if="description" class="expand-section">
|
||||||
<pre class="expand-content">{{ description }}</pre>
|
<pre class="expand-content">{{ description }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="prompt" class="expand-section">
|
||||||
<!-- EXPANDED: Prompt -->
|
|
||||||
<div v-if="showPrompt && prompt" class="expand-section">
|
|
||||||
<pre class="expand-content">{{ prompt }}</pre>
|
<pre class="expand-content">{{ prompt }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="call.result" class="expand-section result-section">
|
||||||
<!-- EXPANDED: Raw result -->
|
|
||||||
<div v-if="showResult && call.result" class="expand-section result-section">
|
|
||||||
<ToolResultBlock :result="call.result" />
|
<ToolResultBlock :result="call.result" />
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -245,7 +221,9 @@ const hasBody = computed(() =>
|
|||||||
padding: 0.3rem 0.6rem;
|
padding: 0.3rem 0.6rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.card-header:hover { background: color-mix(in srgb, var(--card-color) 4%, transparent); }
|
||||||
|
|
||||||
.card-icon { display: flex; align-items: center; color: color-mix(in srgb, var(--card-color) 60%, transparent); flex-shrink: 0; }
|
.card-icon { display: flex; align-items: center; color: color-mix(in srgb, var(--card-color) 60%, transparent); flex-shrink: 0; }
|
||||||
.task-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
.task-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ const ext = computed(() => {
|
|||||||
|
|
||||||
const isError = computed(() => props.call.result?.isError ?? false)
|
const isError = computed(() => props.call.result?.isError ?? false)
|
||||||
|
|
||||||
const contentExpanded = ref(false)
|
const expanded = ref(false)
|
||||||
const resultExpanded = ref(false)
|
|
||||||
|
|
||||||
const highlightedContent = computed(() => highlightCode(content.value, ext.value || undefined))
|
const highlightedContent = computed(() => highlightCode(content.value, ext.value || undefined))
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ const lineCount = computed(() => content.value.split('\n').length)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="['write-card', { error: isError }]">
|
<div :class="['write-card', { error: isError }]">
|
||||||
<div class="card-header">
|
<div class="card-header" @click.stop="expanded = !expanded">
|
||||||
<span class="card-icon">
|
<span class="card-icon">
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="12" height="12" 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"/>
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
@@ -49,28 +48,14 @@ const lineCount = computed(() => content.value.split('\n').length)
|
|||||||
<span v-if="ext" class="ext-badge">.{{ ext }}</span>
|
<span v-if="ext" class="ext-badge">.{{ ext }}</span>
|
||||||
<span class="line-meta">{{ lineCount }}L</span>
|
<span class="line-meta">{{ lineCount }}L</span>
|
||||||
<span v-if="isError" class="error-badge">err</span>
|
<span v-if="isError" class="error-badge">err</span>
|
||||||
|
|
||||||
<span class="header-spacer"></span>
|
|
||||||
|
|
||||||
<button class="toggle-btn" :class="{ active: contentExpanded }" @click.stop="contentExpanded = !contentExpanded" title="Toggle content">
|
|
||||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M12 3v18M3 12h18"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button v-if="call.result" class="toggle-btn" :class="{ active: resultExpanded }" @click.stop="resultExpanded = !resultExpanded" title="Toggle result">
|
|
||||||
<svg width="11" height="11" 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>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content preview -->
|
<template v-if="expanded">
|
||||||
<div v-if="contentExpanded" class="content-section">
|
<div class="content-section">
|
||||||
<pre class="content-pre" v-html="highlightedContent"></pre>
|
<pre class="content-pre" v-html="highlightedContent"></pre>
|
||||||
</div>
|
</div>
|
||||||
|
<ToolResultBlock v-if="call.result" :result="call.result" />
|
||||||
<ToolResultBlock v-if="resultExpanded && call.result" :result="call.result" />
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -93,7 +78,9 @@ const lineCount = computed(() => content.value.split('\n').length)
|
|||||||
padding: 0.3rem 0.6rem;
|
padding: 0.3rem 0.6rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.card-header:hover { background: rgba(34, 197, 94, 0.04); }
|
||||||
|
|
||||||
.card-icon { display: flex; align-items: center; color: rgba(34, 197, 94, 0.6); flex-shrink: 0; }
|
.card-icon { display: flex; align-items: center; color: rgba(34, 197, 94, 0.6); flex-shrink: 0; }
|
||||||
.write-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
.write-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
|
||||||
|
|||||||
Reference in New Issue
Block a user