agent-ecosystem/landing/components/sections/ComparisonSection.vue
iliya e6e89d4ebc fix(tests): improve messageId generation for legacy inbox rows
- Enhanced tests to ensure consistent messageId generation for legacy inbox rows lacking a messageId.
- Updated test descriptions for better clarity regarding the new messageId handling.
- Adjusted test expectations to align with the updated behavior of relaying legacy inbox rows with generated messageIds.
2026-03-23 16:31:37 +02:00

615 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
const { t } = useI18n()
interface CellValue {
status: string
note?: string
}
interface ComparisonRow {
feature: string
us: CellValue
vibeKanban: CellValue
aperant: CellValue
cursor: CellValue
claudeCli: CellValue
}
const rows = computed<ComparisonRow[]>(() => [
{
feature: t('comparison.features.crossTeam'),
us: { status: 'yes' },
vibeKanban: { status: 'no' },
aperant: { status: 'no' },
cursor: { status: 'na' },
claudeCli: { status: 'no' },
},
{
feature: t('comparison.features.agentMessaging'),
us: { status: 'yes', note: 'Native real-time mailbox' },
vibeKanban: { status: 'no', note: 'Agents are independent' },
aperant: { status: 'no', note: 'Fixed pipeline' },
cursor: { status: 'no' },
claudeCli: { status: 'partial', note: 'Built-in (no UI)' },
},
{
feature: t('comparison.features.linkedTasks'),
us: { status: 'yes', note: 'Cross-references in messages' },
vibeKanban: { status: 'partial', note: 'Subtasks only' },
aperant: { status: 'no' },
cursor: { status: 'no' },
claudeCli: { status: 'no' },
},
{
feature: t('comparison.features.sessionAnalysis'),
us: { status: 'yes', note: '6-category token tracking' },
vibeKanban: { status: 'no' },
aperant: { status: 'partial', note: 'Execution logs' },
cursor: { status: 'no' },
claudeCli: { status: 'no' },
},
{
feature: t('comparison.features.taskAttachments'),
us: { status: 'yes', note: 'Auto-attach, agents read & attach' },
vibeKanban: { status: 'no' },
aperant: { status: 'yes', note: 'Images + files' },
cursor: { status: 'partial', note: 'Chat session only' },
claudeCli: { status: 'no' },
},
{
feature: t('comparison.features.hunkReview'),
us: { status: 'yes', note: 'Accept / reject individual hunks' },
vibeKanban: { status: 'no' },
aperant: { status: 'no' },
cursor: { status: 'yes' },
claudeCli: { status: 'no' },
},
{
feature: t('comparison.features.codeEditor'),
us: { status: 'yes', note: 'With Git support' },
vibeKanban: { status: 'no' },
aperant: { status: 'no' },
cursor: { status: 'yes', note: 'Full IDE' },
claudeCli: { status: 'no' },
},
{
feature: t('comparison.features.fullAutonomy'),
us: { status: 'yes', note: 'Create, assign, review end-to-end' },
vibeKanban: { status: 'no', note: 'Human manages tasks' },
aperant: { status: 'no', note: 'Fixed pipeline' },
cursor: { status: 'partial', note: 'Isolated tasks only' },
claudeCli: { status: 'partial', note: 'No UI' },
},
{
feature: t('comparison.features.taskDeps'),
us: { status: 'yes', note: 'Guaranteed ordering' },
vibeKanban: { status: 'no' },
aperant: { status: 'partial', note: 'Within plan only' },
cursor: { status: 'no' },
claudeCli: { status: 'partial', note: 'No UI, no notifications' },
},
{
feature: t('comparison.features.reviewWorkflow'),
us: { status: 'yes', note: 'Agents review each other' },
vibeKanban: { status: 'no' },
aperant: { status: 'partial', note: 'Auto QA pipeline' },
cursor: { status: 'no' },
claudeCli: { status: 'partial', note: 'No UI' },
},
{
feature: t('comparison.features.zeroSetup'),
us: { status: 'yes' },
vibeKanban: { status: 'no', note: 'Config required' },
aperant: { status: 'no', note: 'Config required' },
cursor: { status: 'yes' },
claudeCli: { status: 'partial', note: 'CLI install required' },
},
{
feature: t('comparison.features.kanban'),
us: { status: 'yes', note: '5 columns, real-time' },
vibeKanban: { status: 'yes' },
aperant: { status: 'yes', note: '6 columns (pipeline)' },
cursor: { status: 'no' },
claudeCli: { status: 'no' },
},
{
feature: t('comparison.features.execLog'),
us: { status: 'yes', note: 'Tool calls, reasoning, timeline' },
vibeKanban: { status: 'no' },
aperant: { status: 'yes', note: 'Phase-based logs' },
cursor: { status: 'yes' },
claudeCli: { status: 'no' },
},
{
feature: t('comparison.features.liveProcesses'),
us: { status: 'yes', note: 'View, stop, open URLs' },
vibeKanban: { status: 'no' },
aperant: { status: 'yes', note: '12 agent terminals' },
cursor: { status: 'yes' },
claudeCli: { status: 'no' },
},
{
feature: t('comparison.features.perTaskReview'),
us: { status: 'yes', note: 'Accept / reject / comment' },
vibeKanban: { status: 'partial', note: 'PR-level only' },
aperant: { status: 'partial', note: 'File-level only' },
cursor: { status: 'yes', note: 'BugBot on PRs' },
claudeCli: { status: 'no' },
},
{
feature: t('comparison.features.flexAutonomy'),
us: { status: 'yes', note: 'Granular settings, notifications' },
vibeKanban: { status: 'no' },
aperant: { status: 'partial', note: 'Plan approval only' },
cursor: { status: 'yes' },
claudeCli: { status: 'yes' },
},
{
feature: t('comparison.features.worktree'),
us: { status: 'yes', note: 'Optional' },
vibeKanban: { status: 'partial', note: 'Mandatory' },
aperant: { status: 'partial', note: 'Mandatory' },
cursor: { status: 'yes' },
claudeCli: { status: 'yes' },
},
{
feature: t('comparison.features.multiAgent'),
us: { status: 'soon', note: 'In development' },
vibeKanban: { status: 'yes', note: '6+ agents' },
aperant: { status: 'yes', note: '11 providers' },
cursor: { status: 'yes', note: 'Multi-model' },
claudeCli: { status: 'na' },
},
{
feature: t('comparison.features.price'),
us: { status: 'free' },
vibeKanban: { status: 'text', note: 'Free / $30/mo' },
aperant: { status: 'free' },
cursor: { status: 'text', note: '$0$200/mo' },
claudeCli: { status: 'text', note: 'Claude subscription' },
},
])
const competitors = [
{ key: 'us', name: 'Claude Agent Teams', highlight: true },
{ key: 'vibeKanban', name: 'Vibe Kanban' },
{ key: 'aperant', name: 'Aperant' },
{ key: 'cursor', name: 'Cursor' },
{ key: 'claudeCli', name: 'Claude Code CLI' },
]
function getCellClass(cell: CellValue): string {
switch (cell.status) {
case 'yes': return 'comparison-table__cell--yes'
case 'no': return 'comparison-table__cell--no'
case 'partial': return 'comparison-table__cell--partial'
case 'na': return 'comparison-table__cell--na'
case 'free': return 'comparison-table__cell--free'
case 'soon': return 'comparison-table__cell--soon'
case 'text': return 'comparison-table__cell--text'
default: return 'comparison-table__cell--text'
}
}
function getStatusIcon(status: string): string {
switch (status) {
case 'yes': return '\u2713'
case 'no': return '\u2717'
case 'partial': return '\u25D2'
case 'na': return '\u2014'
case 'free': return 'Free'
case 'soon': return '\uD83D\uDCC5'
default: return ''
}
}
</script>
<template>
<section id="comparison" class="comparison-section section anchor-offset">
<v-container>
<div class="comparison-section__header">
<h2 class="comparison-section__title">
{{ t("comparison.sectionTitle") }}
</h2>
<p class="comparison-section__subtitle">
{{ t("comparison.sectionSubtitle") }}
</p>
</div>
<div class="comparison-table__wrap">
<table class="comparison-table">
<thead>
<tr>
<th class="comparison-table__th comparison-table__th--feature">
{{ t("comparison.feature") }}
</th>
<th
v-for="comp in competitors"
:key="comp.key"
class="comparison-table__th"
:class="{ 'comparison-table__th--highlight': comp.highlight }"
>
{{ comp.name }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, index) in rows"
:key="index"
class="comparison-table__row"
>
<td class="comparison-table__td comparison-table__td--feature">
{{ row.feature }}
</td>
<td
v-for="comp in competitors"
:key="comp.key"
class="comparison-table__td"
:class="[
getCellClass(row[comp.key as keyof ComparisonRow] as CellValue),
{ 'comparison-table__td--highlight-col': comp.highlight }
]"
>
<div class="comparison-table__cell-inner">
<span class="comparison-table__cell-content">
<template v-if="(row[comp.key as keyof ComparisonRow] as CellValue).status === 'text'">
{{ (row[comp.key as keyof ComparisonRow] as CellValue).note }}
</template>
<template v-else>
{{ getStatusIcon((row[comp.key as keyof ComparisonRow] as CellValue).status) }}
</template>
</span>
<span
v-if="(row[comp.key as keyof ComparisonRow] as CellValue).note && (row[comp.key as keyof ComparisonRow] as CellValue).status !== 'text'"
class="comparison-table__cell-note"
>
{{ (row[comp.key as keyof ComparisonRow] as CellValue).note }}
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</v-container>
</section>
</template>
<style scoped>
.comparison-section {
position: relative;
}
.comparison-section__header {
text-align: center;
max-width: 640px;
margin: 0 auto 56px;
position: relative;
z-index: 1;
}
.comparison-section__title {
font-size: 2.4rem;
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1.15;
margin-bottom: 16px;
background: linear-gradient(135deg, #e0e6ff 0%, #00f0ff 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.comparison-section__subtitle {
font-size: 1.1rem;
color: #8892b0;
line-height: 1.6;
margin: 0;
}
/* Table wrapper for horizontal scroll on mobile */
.comparison-table__wrap {
overflow-x: auto;
border-radius: 16px;
border: 1px solid rgba(0, 240, 255, 0.15);
background: rgba(10, 10, 15, 0.6);
backdrop-filter: blur(12px);
position: relative;
z-index: 1;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
min-width: 780px;
font-size: 0.85rem;
}
/* Header */
.comparison-table__th {
padding: 16px 12px;
text-align: center;
font-weight: 600;
font-size: 0.75rem;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #8892b0;
border-bottom: 1px solid rgba(0, 240, 255, 0.1);
white-space: nowrap;
font-family: "JetBrains Mono", monospace;
}
.comparison-table__th--feature {
text-align: left;
padding-left: 20px;
min-width: 180px;
}
.comparison-table__th--highlight {
color: #00f0ff;
background: rgba(0, 240, 255, 0.06);
position: relative;
}
.comparison-table__th--highlight::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, #00f0ff, #39ff14);
}
/* Rows */
.comparison-table__row {
transition: background-color 0.15s ease;
}
.comparison-table__row:hover {
background: rgba(0, 240, 255, 0.03);
}
.comparison-table__row:not(:last-child) .comparison-table__td {
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
/* Cells */
.comparison-table__td {
padding: 10px 8px;
text-align: center;
vertical-align: middle;
}
.comparison-table__td--feature {
text-align: left;
padding-left: 20px;
color: #e0e6ff;
font-weight: 500;
font-size: 0.85rem;
}
.comparison-table__td--highlight-col {
background: rgba(0, 240, 255, 0.04);
}
.comparison-table__cell-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
}
.comparison-table__cell-content {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 700;
}
.comparison-table__cell-note {
font-size: 0.78rem;
color: #6b7994;
line-height: 1.3;
max-width: 140px;
text-align: center;
white-space: normal;
}
/* Cell status variants */
.comparison-table__cell--yes .comparison-table__cell-content {
color: #39ff14;
background: rgba(57, 255, 20, 0.1);
text-shadow: 0 0 8px rgba(57, 255, 20, 0.4);
}
.comparison-table__cell--no .comparison-table__cell-content {
color: #ff4757;
background: rgba(255, 71, 87, 0.08);
opacity: 0.6;
}
.comparison-table__cell--partial .comparison-table__cell-content {
color: #ffd700;
background: rgba(255, 215, 0, 0.08);
}
.comparison-table__cell--na .comparison-table__cell-content {
color: #4a5568;
background: transparent;
}
.comparison-table__cell--soon .comparison-table__cell-content {
width: auto;
padding: 4px 10px;
font-size: 0.75rem;
color: #00f0ff;
background: rgba(0, 240, 255, 0.08);
font-family: "JetBrains Mono", monospace;
}
.comparison-table__cell--free .comparison-table__cell-content,
.comparison-table__cell--text .comparison-table__cell-content {
width: auto;
padding: 4px 10px;
font-size: 0.75rem;
font-family: "JetBrains Mono", monospace;
letter-spacing: 0.04em;
}
.comparison-table__cell--free .comparison-table__cell-content {
color: #39ff14;
background: rgba(57, 255, 20, 0.1);
text-shadow: 0 0 8px rgba(57, 255, 20, 0.4);
}
.comparison-table__cell--text .comparison-table__cell-content {
color: #8892b0;
background: rgba(255, 255, 255, 0.04);
}
/* Highlight column — our product */
.comparison-table__td--highlight-col.comparison-table__cell--yes .comparison-table__cell-content {
box-shadow: 0 0 12px rgba(57, 255, 20, 0.2);
}
.comparison-table__td--highlight-col.comparison-table__cell--free .comparison-table__cell-content {
box-shadow: 0 0 12px rgba(57, 255, 20, 0.2);
}
/* Light theme */
.v-theme--light .comparison-section__title {
background: linear-gradient(135deg, #1e293b 0%, #0891b2 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.v-theme--light .comparison-section__subtitle {
color: #475569;
}
.v-theme--light .comparison-table__wrap {
background: rgba(255, 255, 255, 0.8);
border-color: rgba(0, 180, 200, 0.2);
}
.v-theme--light .comparison-table__th {
color: #64748b;
border-bottom-color: rgba(0, 0, 0, 0.08);
}
.v-theme--light .comparison-table__th--highlight {
color: #0891b2;
background: rgba(8, 145, 178, 0.06);
}
.v-theme--light .comparison-table__th--highlight::after {
background: linear-gradient(90deg, #0891b2, #059669);
}
.v-theme--light .comparison-table__td--feature {
color: #1e293b;
}
.v-theme--light .comparison-table__row:hover {
background: rgba(8, 145, 178, 0.03);
}
.v-theme--light .comparison-table__row:not(:last-child) .comparison-table__td {
border-bottom-color: rgba(0, 0, 0, 0.05);
}
.v-theme--light .comparison-table__td--highlight-col {
background: rgba(8, 145, 178, 0.04);
}
.v-theme--light .comparison-table__cell-note {
color: #94a3b8;
}
.v-theme--light .comparison-table__cell--yes .comparison-table__cell-content {
color: #059669;
background: rgba(5, 150, 105, 0.1);
text-shadow: none;
}
.v-theme--light .comparison-table__cell--no .comparison-table__cell-content {
color: #dc2626;
background: rgba(220, 38, 38, 0.06);
}
.v-theme--light .comparison-table__cell--partial .comparison-table__cell-content {
color: #d97706;
background: rgba(217, 119, 6, 0.08);
}
.v-theme--light .comparison-table__cell--free .comparison-table__cell-content {
color: #059669;
background: rgba(5, 150, 105, 0.1);
text-shadow: none;
}
.v-theme--light .comparison-table__cell--soon .comparison-table__cell-content {
color: #0891b2;
background: rgba(8, 145, 178, 0.08);
}
.v-theme--light .comparison-table__cell--text .comparison-table__cell-content {
color: #64748b;
background: rgba(0, 0, 0, 0.04);
}
/* Responsive */
@media (max-width: 960px) {
.comparison-section__title {
font-size: 1.85rem;
}
.comparison-section__header {
margin-bottom: 40px;
}
.comparison-section__subtitle {
font-size: 1rem;
}
}
@media (max-width: 600px) {
.comparison-section__title {
font-size: 1.6rem;
}
.comparison-section__header {
margin-bottom: 32px;
}
.comparison-table {
font-size: 0.8rem;
}
.comparison-table__th {
padding: 12px 8px;
font-size: 0.7rem;
}
.comparison-table__td {
padding: 8px 6px;
}
.comparison-table__td--feature {
padding-left: 14px;
font-size: 0.8rem;
}
.comparison-table__cell-note {
font-size: 0.7rem;
max-width: 110px;
}
}
</style>