claude-teammate 0.1.293 → 0.1.294
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -124,6 +124,15 @@
|
|
|
124
124
|
</span>
|
|
125
125
|
<span>Usage</span>
|
|
126
126
|
</NuxtLink>
|
|
127
|
+
<NuxtLink to="/skills" class="nav-item" :class="{ active: $route.path === '/skills' }" @click="closeSidebar">
|
|
128
|
+
<span class="nav-ico">
|
|
129
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
130
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437 1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008Z" />
|
|
131
|
+
</svg>
|
|
132
|
+
</span>
|
|
133
|
+
<span>Skills</span>
|
|
134
|
+
<span class="nav-count violet" v-if="skillsCount !== null && skillsCount > 0">{{ skillsCount }}</span>
|
|
135
|
+
</NuxtLink>
|
|
127
136
|
</div>
|
|
128
137
|
</nav>
|
|
129
138
|
|
|
@@ -150,14 +159,18 @@
|
|
|
150
159
|
<NuxtLink to="/logs" class="bottom-nav-item" :class="{ active: $route.path === '/logs' }"><span>≡</span>Logs</NuxtLink>
|
|
151
160
|
<NuxtLink to="/activity" class="bottom-nav-item" :class="{ active: $route.path === '/activity' }"><span>◉</span>Activity</NuxtLink>
|
|
152
161
|
<NuxtLink to="/usage" class="bottom-nav-item" :class="{ active: $route.path === '/usage' }"><span>$</span>Usage</NuxtLink>
|
|
162
|
+
<NuxtLink to="/skills" class="bottom-nav-item" :class="{ active: $route.path === '/skills' }"><span>⚙</span>Skills</NuxtLink>
|
|
153
163
|
</nav>
|
|
154
164
|
</template>
|
|
155
165
|
|
|
156
166
|
<script setup lang="ts">
|
|
157
167
|
const { status, startPolling } = useStatus();
|
|
168
|
+
const { skillFixStats, loadSkillFixStats, startPolling: startSkillPolling } = useSkillFixes();
|
|
158
169
|
const sidebarOpen = ref(false);
|
|
159
170
|
const clockText = ref("");
|
|
160
171
|
|
|
172
|
+
const _skillsCount = computed(() => skillFixStats.value?.totalEvents24h ?? null);
|
|
173
|
+
|
|
161
174
|
function _toggleSidebar() {
|
|
162
175
|
sidebarOpen.value = !sidebarOpen.value;
|
|
163
176
|
}
|
|
@@ -195,6 +208,8 @@ const _awaitingCount = computed(() => {
|
|
|
195
208
|
|
|
196
209
|
onMounted(() => {
|
|
197
210
|
startPolling();
|
|
211
|
+
startSkillPolling(15000);
|
|
212
|
+
loadSkillFixStats();
|
|
198
213
|
clockText.value = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
199
214
|
setInterval(() => {
|
|
200
215
|
clockText.value = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
@@ -65,11 +65,10 @@
|
|
|
65
65
|
<div class="stat-sub">Queued for retry</div>
|
|
66
66
|
<div class="stat-progress"><div class="stat-progress-fill" :style="{ width: stuckBarPct + '%', background: 'var(--red)' }"></div></div>
|
|
67
67
|
</div>
|
|
68
|
-
<
|
|
68
|
+
<NuxtLink
|
|
69
69
|
class="stat-card stat-card-link"
|
|
70
70
|
v-if="skillFixStats && skillFixStats.totalEvents24h > 0"
|
|
71
|
-
|
|
72
|
-
@click.prevent="scrollToSkillCard"
|
|
71
|
+
to="/skills"
|
|
73
72
|
>
|
|
74
73
|
<div class="stat-card-accent" style="background:linear-gradient(90deg,var(--violet),var(--sky))"></div>
|
|
75
74
|
<div class="stat-label">Skills</div>
|
|
@@ -80,7 +79,7 @@
|
|
|
80
79
|
<div class="stat-progress">
|
|
81
80
|
<div class="stat-progress-fill" :style="{ width: skillSuccessPct + '%', background: 'var(--violet)' }"></div>
|
|
82
81
|
</div>
|
|
83
|
-
</
|
|
82
|
+
</NuxtLink>
|
|
84
83
|
</div>
|
|
85
84
|
|
|
86
85
|
<!-- STUCK TASKS TABLE -->
|
|
@@ -292,111 +291,6 @@
|
|
|
292
291
|
</div>
|
|
293
292
|
</template>
|
|
294
293
|
|
|
295
|
-
<!-- SKILL SELF-REPAIR -->
|
|
296
|
-
<div class="card" v-if="skillFixes.length > 0" id="skill-self-repair" ref="skillCardRef">
|
|
297
|
-
<div class="card-header">
|
|
298
|
-
<span class="card-title">⚙ Skill Self-Repair</span>
|
|
299
|
-
<span v-if="skillFixStats" style="font-family:var(--f-mono);font-size:.7rem;color:var(--ink-3)">
|
|
300
|
-
{{ skillFixStats.totalEvents24h }} events 24h
|
|
301
|
-
<span v-if="skillSuccessLabel"> • {{ skillSuccessLabel }}</span>
|
|
302
|
-
<span v-if="skillFixStats.cooldownActive.length > 0"> • {{ skillFixStats.cooldownActive.length }} cooldown</span>
|
|
303
|
-
</span>
|
|
304
|
-
</div>
|
|
305
|
-
<!-- Active fixes banner -->
|
|
306
|
-
<div v-if="activeSkillFixes.length > 0" style="padding:8px 12px;background:rgba(234,179,8,.08);border-bottom:1px solid rgba(234,179,8,.2);font-size:.8rem;color:var(--ink-2)">
|
|
307
|
-
<span style="color:#ca8a04;font-weight:600">Fixing:</span>
|
|
308
|
-
<span v-for="f in activeSkillFixes" :key="f.skill + f.ts" style="margin-left:8px">
|
|
309
|
-
<span style="font-family:var(--f-mono);color:var(--ink-1);cursor:pointer;text-decoration:underline" @click="openSkillDrawer(f.skill)">{{ f.skill }}</span>
|
|
310
|
-
<span style="color:var(--ink-3);margin-left:4px">({{ f.location || '?' }})</span>
|
|
311
|
-
</span>
|
|
312
|
-
</div>
|
|
313
|
-
|
|
314
|
-
<!-- Top failing skills -->
|
|
315
|
-
<div v-if="skillFixStats && skillFixStats.topFailingSkills.length > 0" style="padding:10px 12px;border-bottom:1px solid var(--border-color)">
|
|
316
|
-
<div style="font-size:.65rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--ink-3);margin-bottom:6px">Top problem skills (24h)</div>
|
|
317
|
-
<div style="display:flex;flex-direction:column;gap:4px">
|
|
318
|
-
<div v-for="s in skillFixStats.topFailingSkills.slice(0, 5)" :key="s.skill" class="topfail-row">
|
|
319
|
-
<span class="topfail-name" @click="openSkillDrawer(s.skill)">{{ s.skill }}</span>
|
|
320
|
-
<div class="topfail-bar"><div class="topfail-bar-fill" :style="{ width: topFailBarPct(s.errorCount) + '%' }"></div></div>
|
|
321
|
-
<span class="topfail-count">{{ s.errorCount }} err</span>
|
|
322
|
-
<span v-if="s.fixCount > 0" class="topfail-fixes">{{ s.fixCount }} ✓</span>
|
|
323
|
-
</div>
|
|
324
|
-
</div>
|
|
325
|
-
</div>
|
|
326
|
-
|
|
327
|
-
<!-- Improvement opportunities -->
|
|
328
|
-
<div v-if="improvementEvents.length > 0" style="padding:10px 12px;border-bottom:1px solid var(--border-color)">
|
|
329
|
-
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
|
|
330
|
-
<div style="font-size:.65rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--ink-3)">
|
|
331
|
-
Improvement opportunities ({{ improvementEvents.length }})
|
|
332
|
-
</div>
|
|
333
|
-
<label style="font-size:.7rem;color:var(--ink-3);cursor:pointer">
|
|
334
|
-
<input type="checkbox" v-model="hideImprovements" /> hide
|
|
335
|
-
</label>
|
|
336
|
-
</div>
|
|
337
|
-
<div v-if="!hideImprovements" style="display:flex;flex-direction:column;gap:4px">
|
|
338
|
-
<div v-for="ev in improvementEvents.slice(0, 5)" :key="ev.ts + ev.skill" style="display:flex;align-items:center;gap:8px;font-size:.78rem">
|
|
339
|
-
<span style="font-family:var(--f-mono);font-weight:600;cursor:pointer;text-decoration:underline" @click="openSkillDrawer(ev.skill)">{{ ev.skill }}</span>
|
|
340
|
-
<span style="color:var(--ink-3);font-size:.72rem">{{ skillFixTriggerLabel(ev.errorType) }}</span>
|
|
341
|
-
<a v-if="ev.prUrl" :href="ev.prUrl" target="_blank" rel="noreferrer" style="margin-left:auto;color:var(--sky);font-size:.72rem">PR →</a>
|
|
342
|
-
</div>
|
|
343
|
-
</div>
|
|
344
|
-
</div>
|
|
345
|
-
|
|
346
|
-
<div class="table-wrap">
|
|
347
|
-
<table>
|
|
348
|
-
<thead>
|
|
349
|
-
<tr>
|
|
350
|
-
<th>Time</th>
|
|
351
|
-
<th>Skill</th>
|
|
352
|
-
<th>Location</th>
|
|
353
|
-
<th>Trigger</th>
|
|
354
|
-
<th>Mode</th>
|
|
355
|
-
<th>Status</th>
|
|
356
|
-
<th>Strategy</th>
|
|
357
|
-
<th>Cooldown</th>
|
|
358
|
-
<th>Details</th>
|
|
359
|
-
</tr>
|
|
360
|
-
</thead>
|
|
361
|
-
<tbody>
|
|
362
|
-
<tr v-for="ev in skillFixes.slice(0, 20)" :key="ev.skill + ev.ts" class="clickable" @click="openSkillDrawer(ev.skill)" title="View skill details">
|
|
363
|
-
<td style="font-family:var(--f-mono);font-size:.75rem;color:var(--ink-3);white-space:nowrap">{{ formatRelative(ev.ts) }}</td>
|
|
364
|
-
<td style="font-family:var(--f-mono);font-weight:600">{{ ev.skill }}</td>
|
|
365
|
-
<td>
|
|
366
|
-
<span v-if="ev.location" :class="['tag', ev.location === 'repo' ? 'sky' : 'violet']">{{ ev.location }}</span>
|
|
367
|
-
<span v-else style="color:var(--ink-3)">—</span>
|
|
368
|
-
</td>
|
|
369
|
-
<td style="font-size:.75rem;color:var(--ink-2)">{{ skillFixTriggerLabel(ev.errorType) }}</td>
|
|
370
|
-
<td>
|
|
371
|
-
<span v-if="ev.mode" :class="['tag', ev.mode === 'improve' ? 'sky' : 'red']" style="font-size:.65rem">{{ ev.mode }}</span>
|
|
372
|
-
<span v-else style="color:var(--ink-3)">—</span>
|
|
373
|
-
</td>
|
|
374
|
-
<td>
|
|
375
|
-
<span :class="['tag', skillFixStatusClass(ev.status)]">{{ ev.status }}</span>
|
|
376
|
-
</td>
|
|
377
|
-
<td>
|
|
378
|
-
<span v-if="ev.strategy" class="tag" style="font-size:.65rem">{{ ev.strategy }}</span>
|
|
379
|
-
<span v-else style="color:var(--ink-3)">—</span>
|
|
380
|
-
</td>
|
|
381
|
-
<td>
|
|
382
|
-
<span
|
|
383
|
-
v-if="typeof ev.recentFixCount === 'number'"
|
|
384
|
-
:style="{ fontFamily: 'var(--f-mono)', fontSize: '.72rem', color: ev.recentFixCount >= 2 ? 'var(--red)' : 'var(--ink-3)' }"
|
|
385
|
-
>{{ ev.recentFixCount }}</span>
|
|
386
|
-
<span v-else style="color:var(--ink-3)">—</span>
|
|
387
|
-
</td>
|
|
388
|
-
<td style="font-size:.75rem" @click.stop>
|
|
389
|
-
<a v-if="ev.prUrl" :href="ev.prUrl" target="_blank" style="color:var(--sky);text-decoration:none">PR →</a>
|
|
390
|
-
<span v-else-if="ev.error" style="color:var(--red);font-family:var(--f-mono)" :title="ev.error">{{ ev.error.slice(0, 40) }}…</span>
|
|
391
|
-
<span v-else-if="ev.files" style="color:var(--ink-3)">{{ ev.files }} file{{ ev.files !== 1 ? 's' : '' }}</span>
|
|
392
|
-
<span v-else style="color:var(--ink-3)">—</span>
|
|
393
|
-
</td>
|
|
394
|
-
</tr>
|
|
395
|
-
</tbody>
|
|
396
|
-
</table>
|
|
397
|
-
</div>
|
|
398
|
-
</div>
|
|
399
|
-
|
|
400
294
|
<!-- ISSUE DRAWER (always mounted) -->
|
|
401
295
|
<IssueDrawer
|
|
402
296
|
:issue="drawerIssue"
|
|
@@ -405,28 +299,14 @@
|
|
|
405
299
|
@close="closeDrawer"
|
|
406
300
|
@reset-done="refresh"
|
|
407
301
|
/>
|
|
408
|
-
|
|
409
|
-
<!-- SKILL FIX DRAWER -->
|
|
410
|
-
<SkillFixDrawer
|
|
411
|
-
:skill="skillDrawerSkill"
|
|
412
|
-
:open="skillDrawerOpen"
|
|
413
|
-
@close="closeSkillDrawer"
|
|
414
|
-
@rolled="refresh"
|
|
415
|
-
/>
|
|
416
302
|
</div>
|
|
417
303
|
</template>
|
|
418
304
|
|
|
419
305
|
<script setup lang="ts">
|
|
420
|
-
import type { DraftPr, ReviewPr,
|
|
306
|
+
import type { DraftPr, ReviewPr, StuckTask, WorkflowIssue } from "~/composables/useStatus";
|
|
421
307
|
|
|
422
308
|
const { status, loadStatus } = useStatus();
|
|
423
|
-
const {
|
|
424
|
-
skillFixes,
|
|
425
|
-
skillFixStats,
|
|
426
|
-
loadSkillFixes,
|
|
427
|
-
loadSkillFixStats,
|
|
428
|
-
startPolling: startSkillPolling
|
|
429
|
-
} = useSkillFixes();
|
|
309
|
+
const { skillFixStats, loadSkillFixes, loadSkillFixStats, startPolling: startSkillPolling } = useSkillFixes();
|
|
430
310
|
const { apiFetch } = useApi();
|
|
431
311
|
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
432
312
|
const { formatTime, formatRelative, formatDuration, stateLabel, shortenUrl } = useHelpers();
|
|
@@ -610,27 +490,6 @@ function closeDrawer() {
|
|
|
610
490
|
drawerOpen.value = false;
|
|
611
491
|
}
|
|
612
492
|
|
|
613
|
-
// Stale-guard: a worker crash mid-generation leaves a "generating" event in the
|
|
614
|
-
// log forever. Treat events older than 5 minutes as stale so the banner doesn't
|
|
615
|
-
// show ghost fixes.
|
|
616
|
-
const ACTIVE_FIX_STALE_MS = 5 * 60 * 1000;
|
|
617
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
618
|
-
const activeSkillFixes = computed(() =>
|
|
619
|
-
(skillFixes.value as SkillFixEvent[]).filter(
|
|
620
|
-
(e) => e.status === "generating" && Date.now() - new Date(e.ts).getTime() < ACTIVE_FIX_STALE_MS
|
|
621
|
-
)
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
625
|
-
const improvementEvents = computed(() =>
|
|
626
|
-
(skillFixes.value as SkillFixEvent[]).filter(
|
|
627
|
-
(e) => e.mode === "improve" && e.errorType === "skill-improvement-opportunity"
|
|
628
|
-
)
|
|
629
|
-
);
|
|
630
|
-
|
|
631
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
632
|
-
const hideImprovements = ref(false);
|
|
633
|
-
|
|
634
493
|
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
635
494
|
const skillSuccessLabel = computed(() => {
|
|
636
495
|
const r = skillFixStats.value?.successRate24h;
|
|
@@ -645,58 +504,6 @@ const skillSuccessPct = computed(() => {
|
|
|
645
504
|
return Math.round(r * 100);
|
|
646
505
|
});
|
|
647
506
|
|
|
648
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
649
|
-
function topFailBarPct(count: number): number {
|
|
650
|
-
const max = skillFixStats.value?.topFailingSkills[0]?.errorCount || 1;
|
|
651
|
-
return Math.min(100, (count / Math.max(max, 1)) * 100);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
655
|
-
function skillFixStatusClass(status: SkillFixEvent["status"]): string {
|
|
656
|
-
if (status === "pr-created" || status === "patched" || status === "patched-with-backup") return "green";
|
|
657
|
-
if (status === "generating") return "yellow";
|
|
658
|
-
if (status === "patch-failed") return "orange";
|
|
659
|
-
if (status === "cooldown") return "violet";
|
|
660
|
-
if (status === "error" || status === "no-fix" || status === "generation-error") return "red";
|
|
661
|
-
if (status === "no-op" || status === "pr-exists") return "sky";
|
|
662
|
-
if (status === "lock-skipped" || status === "not-found" || status === "scope-excluded") return "violet";
|
|
663
|
-
return "";
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
667
|
-
function skillFixTriggerLabel(errorType?: string): string {
|
|
668
|
-
if (!errorType) return "—";
|
|
669
|
-
if (errorType === "user-feedback") return "user feedback";
|
|
670
|
-
if (errorType === "bash-error-in-skill") return "bash error";
|
|
671
|
-
if (errorType === "tool-error-in-skill") return "mcp error";
|
|
672
|
-
if (errorType === "skill-load-failed") return "load failed";
|
|
673
|
-
if (errorType === "skill-bypassed") return "bypassed";
|
|
674
|
-
if (errorType === "skill-caused-retry") return "silent retry";
|
|
675
|
-
if (errorType === "skill-caused-fallback") return "silent fallback";
|
|
676
|
-
if (errorType === "skill-improvement-opportunity") return "improve";
|
|
677
|
-
return errorType;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const skillCardRef = ref<HTMLElement | null>(null);
|
|
681
|
-
const skillDrawerOpen = ref(false);
|
|
682
|
-
const skillDrawerSkill = ref<string | null>(null);
|
|
683
|
-
|
|
684
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
685
|
-
function openSkillDrawer(skill: string) {
|
|
686
|
-
skillDrawerSkill.value = skill;
|
|
687
|
-
skillDrawerOpen.value = true;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
691
|
-
function closeSkillDrawer() {
|
|
692
|
-
skillDrawerOpen.value = false;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
696
|
-
function scrollToSkillCard() {
|
|
697
|
-
skillCardRef.value?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
698
|
-
}
|
|
699
|
-
|
|
700
507
|
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
701
508
|
async function refresh() {
|
|
702
509
|
await Promise.all([loadStatus(), loadSkillFixes(), loadSkillFixStats()]);
|
|
@@ -797,48 +604,4 @@ onMounted(async () => {
|
|
|
797
604
|
transition: transform .12s ease;
|
|
798
605
|
}
|
|
799
606
|
.stat-card-link:hover { transform: translateY(-1px); }
|
|
800
|
-
|
|
801
|
-
.topfail-row {
|
|
802
|
-
display: flex;
|
|
803
|
-
align-items: center;
|
|
804
|
-
gap: 8px;
|
|
805
|
-
font-size: .78rem;
|
|
806
|
-
}
|
|
807
|
-
.topfail-name {
|
|
808
|
-
font-family: var(--f-mono);
|
|
809
|
-
font-weight: 600;
|
|
810
|
-
color: var(--ink-1);
|
|
811
|
-
cursor: pointer;
|
|
812
|
-
text-decoration: underline;
|
|
813
|
-
min-width: 100px;
|
|
814
|
-
max-width: 220px;
|
|
815
|
-
overflow: hidden;
|
|
816
|
-
text-overflow: ellipsis;
|
|
817
|
-
white-space: nowrap;
|
|
818
|
-
}
|
|
819
|
-
.topfail-bar {
|
|
820
|
-
flex: 1;
|
|
821
|
-
height: 6px;
|
|
822
|
-
background: var(--bg-input);
|
|
823
|
-
border-radius: 3px;
|
|
824
|
-
overflow: hidden;
|
|
825
|
-
min-width: 60px;
|
|
826
|
-
}
|
|
827
|
-
.topfail-bar-fill {
|
|
828
|
-
height: 100%;
|
|
829
|
-
background: var(--red);
|
|
830
|
-
transition: width .2s;
|
|
831
|
-
}
|
|
832
|
-
.topfail-count {
|
|
833
|
-
font-family: var(--f-mono);
|
|
834
|
-
font-size: .72rem;
|
|
835
|
-
color: var(--red);
|
|
836
|
-
min-width: 50px;
|
|
837
|
-
text-align: right;
|
|
838
|
-
}
|
|
839
|
-
.topfail-fixes {
|
|
840
|
-
font-family: var(--f-mono);
|
|
841
|
-
font-size: .72rem;
|
|
842
|
-
color: var(--green);
|
|
843
|
-
}
|
|
844
607
|
</style>
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="page-in">
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<div>
|
|
5
|
+
<div class="page-title">Skill Self-Repair</div>
|
|
6
|
+
<div class="page-sub">{{ subLine }}</div>
|
|
7
|
+
</div>
|
|
8
|
+
<button class="btn" @click="refresh">↻ Refresh</button>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<template v-if="skillFixes.length === 0 && !loading">
|
|
12
|
+
<div class="card" style="padding:24px;text-align:center;color:var(--ink-3)">
|
|
13
|
+
No skill self-repair events recorded.
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<template v-else>
|
|
18
|
+
<!-- STAT GRID -->
|
|
19
|
+
<div class="stat-grid">
|
|
20
|
+
<div class="stat-card">
|
|
21
|
+
<div class="stat-card-accent" style="background:linear-gradient(90deg,var(--violet),var(--sky))"></div>
|
|
22
|
+
<div class="stat-label">Events 24h</div>
|
|
23
|
+
<div class="stat-value">{{ skillFixStats?.totalEvents24h ?? 0 }}</div>
|
|
24
|
+
<div class="stat-sub">{{ skillFixStats?.totalEvents7d ?? 0 }} in 7d</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="stat-card">
|
|
27
|
+
<div class="stat-card-accent green"></div>
|
|
28
|
+
<div class="stat-label">Success Rate</div>
|
|
29
|
+
<div class="stat-value">{{ skillSuccessLabel || '—' }}</div>
|
|
30
|
+
<div class="stat-progress"><div class="stat-progress-fill" :style="{ width: skillSuccessPct + '%', background: 'var(--green)' }"></div></div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="stat-card">
|
|
33
|
+
<div class="stat-card-accent violet"></div>
|
|
34
|
+
<div class="stat-label">Cooldown</div>
|
|
35
|
+
<div class="stat-value">{{ skillFixStats?.cooldownActive.length ?? 0 }}</div>
|
|
36
|
+
<div class="stat-sub">{{ skillFixStats?.cooldownActive.length ? 'paused skills' : 'none' }}</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="stat-card">
|
|
39
|
+
<div class="stat-card-accent sky"></div>
|
|
40
|
+
<div class="stat-label">Improvements</div>
|
|
41
|
+
<div class="stat-value">{{ skillFixStats?.improvementOpportunities24h ?? 0 }}</div>
|
|
42
|
+
<div class="stat-sub">opportunities 24h</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="stat-card">
|
|
45
|
+
<div class="stat-card-accent orange"></div>
|
|
46
|
+
<div class="stat-label">Active Fixes</div>
|
|
47
|
+
<div class="stat-value">{{ activeSkillFixes.length }}</div>
|
|
48
|
+
<div class="stat-sub">in progress</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- ACTIVE FIXES BANNER -->
|
|
53
|
+
<div class="card" v-if="activeSkillFixes.length > 0" style="padding:10px 14px;background:rgba(234,179,8,.06);border-color:rgba(234,179,8,.3)">
|
|
54
|
+
<span style="color:#ca8a04;font-weight:600;font-size:.78rem">Currently fixing:</span>
|
|
55
|
+
<span v-for="f in activeSkillFixes" :key="f.skill + f.ts" style="margin-left:10px;font-size:.78rem">
|
|
56
|
+
<span style="font-family:var(--f-mono);cursor:pointer;text-decoration:underline" @click="openSkillDrawer(f.skill)">{{ f.skill }}</span>
|
|
57
|
+
<span style="color:var(--ink-3);margin-left:4px">({{ f.location || '?' }})</span>
|
|
58
|
+
</span>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- TOP FAILING -->
|
|
62
|
+
<div class="card" v-if="skillFixStats && skillFixStats.topFailingSkills.length > 0">
|
|
63
|
+
<div class="card-header">
|
|
64
|
+
<span class="card-title">Top problem skills (24h)</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div style="padding:10px 14px;display:flex;flex-direction:column;gap:6px">
|
|
67
|
+
<div v-for="s in skillFixStats.topFailingSkills" :key="s.skill" class="topfail-row">
|
|
68
|
+
<span class="topfail-name" @click="openSkillDrawer(s.skill)">{{ s.skill }}</span>
|
|
69
|
+
<div class="topfail-bar"><div class="topfail-bar-fill" :style="{ width: topFailBarPct(s.errorCount) + '%' }"></div></div>
|
|
70
|
+
<span class="topfail-count">{{ s.errorCount }} err</span>
|
|
71
|
+
<span v-if="s.fixCount > 0" class="topfail-fixes">{{ s.fixCount }} ✓</span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- IMPROVEMENT OPPORTUNITIES -->
|
|
77
|
+
<div class="card" v-if="improvementEvents.length > 0">
|
|
78
|
+
<div class="card-header">
|
|
79
|
+
<span class="card-title">Improvement opportunities</span>
|
|
80
|
+
<span style="font-family:var(--f-mono);font-size:.7rem;color:var(--ink-3)">{{ improvementEvents.length }} total</span>
|
|
81
|
+
</div>
|
|
82
|
+
<div style="padding:10px 14px;display:flex;flex-direction:column;gap:6px">
|
|
83
|
+
<div v-for="ev in improvementEvents.slice(0, 10)" :key="ev.ts + ev.skill" style="display:flex;align-items:center;gap:8px;font-size:.78rem">
|
|
84
|
+
<span style="font-family:var(--f-mono);font-weight:600;cursor:pointer;text-decoration:underline" @click="openSkillDrawer(ev.skill)">{{ ev.skill }}</span>
|
|
85
|
+
<span style="color:var(--ink-3);font-size:.72rem">{{ skillFixTriggerLabel(ev.errorType) }}</span>
|
|
86
|
+
<span style="color:var(--ink-3);font-size:.7rem;margin-left:auto">{{ formatRelative(ev.ts) }}</span>
|
|
87
|
+
<a v-if="ev.prUrl" :href="ev.prUrl" target="_blank" rel="noreferrer" style="color:var(--sky);font-size:.72rem">PR →</a>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- GROUPED TABLE -->
|
|
93
|
+
<div class="card">
|
|
94
|
+
<div class="card-header">
|
|
95
|
+
<span class="card-title">Skills</span>
|
|
96
|
+
<div style="display:flex;align-items:center;gap:10px">
|
|
97
|
+
<input
|
|
98
|
+
v-model="filterKw"
|
|
99
|
+
placeholder="Filter skill…"
|
|
100
|
+
class="fsel"
|
|
101
|
+
style="height:30px;font-size:.78rem;width:180px"
|
|
102
|
+
/>
|
|
103
|
+
<span style="font-family:var(--f-mono);font-size:.7rem;color:var(--ink-3)">
|
|
104
|
+
{{ filteredGroups.length }} skill{{ filteredGroups.length !== 1 ? 's' : '' }}
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="table-wrap">
|
|
109
|
+
<table>
|
|
110
|
+
<thead>
|
|
111
|
+
<tr>
|
|
112
|
+
<th style="width:24px"></th>
|
|
113
|
+
<th><button class="sort-btn" :class="{ 'sort-active': sort.col==='skill' }" @click="toggleSort('skill')">Skill {{ sortIco('skill') }}</button></th>
|
|
114
|
+
<th><button class="sort-btn" :class="{ 'sort-active': sort.col==='total' }" @click="toggleSort('total')">Events {{ sortIco('total') }}</button></th>
|
|
115
|
+
<th><button class="sort-btn" :class="{ 'sort-active': sort.col==='errors' }" @click="toggleSort('errors')">Errors {{ sortIco('errors') }}</button></th>
|
|
116
|
+
<th><button class="sort-btn" :class="{ 'sort-active': sort.col==='success' }" @click="toggleSort('success')">✓ {{ sortIco('success') }}</button></th>
|
|
117
|
+
<th class="max-[860px]:hidden">Location</th>
|
|
118
|
+
<th class="max-[860px]:hidden">Last status</th>
|
|
119
|
+
<th class="max-[640px]:hidden"><button class="sort-btn" :class="{ 'sort-active': sort.col==='lastTs' }" @click="toggleSort('lastTs')">Last activity {{ sortIco('lastTs') }}</button></th>
|
|
120
|
+
<th>Last detail</th>
|
|
121
|
+
</tr>
|
|
122
|
+
</thead>
|
|
123
|
+
<tbody>
|
|
124
|
+
<template v-for="g in filteredGroups" :key="g.skill">
|
|
125
|
+
<tr class="clickable" @click="toggleExpand(g.skill)" :title="expanded.has(g.skill) ? 'Collapse' : 'Expand'">
|
|
126
|
+
<td style="text-align:center;color:var(--ink-3);font-family:var(--f-mono);font-size:.7rem">
|
|
127
|
+
{{ expanded.has(g.skill) ? '▾' : '▸' }}
|
|
128
|
+
</td>
|
|
129
|
+
<td style="font-family:var(--f-mono);font-weight:600">
|
|
130
|
+
<span @click.stop="openSkillDrawer(g.skill)" style="cursor:pointer;text-decoration:underline">{{ g.skill }}</span>
|
|
131
|
+
</td>
|
|
132
|
+
<td>{{ g.total }}</td>
|
|
133
|
+
<td>
|
|
134
|
+
<span :style="{ color: g.errors > 0 ? 'var(--red)' : 'var(--ink-3)', fontWeight: g.errors > 0 ? 600 : 400 }">{{ g.errors }}</span>
|
|
135
|
+
</td>
|
|
136
|
+
<td>
|
|
137
|
+
<span :style="{ color: g.success > 0 ? 'var(--green)' : 'var(--ink-3)' }">{{ g.success }}</span>
|
|
138
|
+
</td>
|
|
139
|
+
<td class="max-[860px]:hidden">
|
|
140
|
+
<span v-if="g.location" :class="['tag', g.location === 'repo' ? 'sky' : 'violet']" style="font-size:.65rem">{{ g.location }}</span>
|
|
141
|
+
<span v-else style="color:var(--ink-3)">—</span>
|
|
142
|
+
</td>
|
|
143
|
+
<td class="max-[860px]:hidden">
|
|
144
|
+
<span :class="['tag', skillFixStatusClass(g.lastStatus)]" style="font-size:.65rem">{{ g.lastStatus }}</span>
|
|
145
|
+
</td>
|
|
146
|
+
<td class="max-[640px]:hidden" style="font-family:var(--f-mono);font-size:.74rem;color:var(--ink-3);white-space:nowrap">{{ formatRelative(g.lastTs) }}</td>
|
|
147
|
+
<td style="font-size:.74rem;max-width:260px" @click.stop>
|
|
148
|
+
<a v-if="g.lastPrUrl" :href="g.lastPrUrl" target="_blank" rel="noreferrer" style="color:var(--sky);text-decoration:none">PR →</a>
|
|
149
|
+
<span v-else-if="g.lastError" style="color:var(--red);font-family:var(--f-mono);display:inline-block;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle" :title="g.lastError">{{ g.lastError }}</span>
|
|
150
|
+
<span v-else-if="g.lastFiles" style="color:var(--ink-3)">{{ g.lastFiles }} file{{ g.lastFiles !== 1 ? 's' : '' }}</span>
|
|
151
|
+
<span v-else style="color:var(--ink-3)">—</span>
|
|
152
|
+
</td>
|
|
153
|
+
</tr>
|
|
154
|
+
<!-- EXPANDED ROW -->
|
|
155
|
+
<tr v-if="expanded.has(g.skill)" class="expanded-row">
|
|
156
|
+
<td></td>
|
|
157
|
+
<td colspan="8" style="padding:0">
|
|
158
|
+
<div style="padding:10px 14px;background:var(--bg-alt);border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color)">
|
|
159
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
|
|
160
|
+
<span style="font-size:.68rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--ink-3)">All events ({{ g.events.length }})</span>
|
|
161
|
+
<button class="btn" style="height:24px;padding:0 8px;font-size:.7rem;margin-left:auto" @click.stop="openSkillDrawer(g.skill)">Open drawer →</button>
|
|
162
|
+
</div>
|
|
163
|
+
<div style="display:flex;flex-direction:column;gap:6px">
|
|
164
|
+
<div v-for="(ev, idx) in g.events" :key="ev.ts + idx" class="ev-line">
|
|
165
|
+
<div class="ev-line-head">
|
|
166
|
+
<span style="font-family:var(--f-mono);font-size:.7rem;color:var(--ink-3);min-width:90px">{{ formatRelative(ev.ts) }}</span>
|
|
167
|
+
<span :class="['tag', skillFixStatusClass(ev.status)]" style="font-size:.62rem">{{ ev.status }}</span>
|
|
168
|
+
<span v-if="ev.mode" :class="['tag', ev.mode === 'improve' ? 'sky' : 'red']" style="font-size:.62rem">{{ ev.mode }}</span>
|
|
169
|
+
<span v-if="ev.strategy" class="tag" style="font-size:.62rem">{{ ev.strategy }}</span>
|
|
170
|
+
<span style="font-size:.7rem;color:var(--ink-2)">{{ skillFixTriggerLabel(ev.errorType) }}</span>
|
|
171
|
+
<span v-if="typeof ev.recentFixCount === 'number' && ev.recentFixCount > 0" style="font-family:var(--f-mono);font-size:.68rem;color:var(--ink-3);margin-left:auto">cooldown:{{ ev.recentFixCount }}</span>
|
|
172
|
+
<a v-if="ev.prUrl" :href="ev.prUrl" target="_blank" rel="noreferrer" style="color:var(--sky);font-size:.7rem;margin-left:auto">PR →</a>
|
|
173
|
+
</div>
|
|
174
|
+
<div v-if="ev.error" class="ev-line-error">{{ ev.error }}</div>
|
|
175
|
+
<div v-else-if="ev.files" style="font-size:.7rem;color:var(--ink-3);margin-top:2px">{{ ev.files }} file{{ ev.files !== 1 ? 's' : '' }} processed</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</td>
|
|
180
|
+
</tr>
|
|
181
|
+
</template>
|
|
182
|
+
</tbody>
|
|
183
|
+
</table>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</template>
|
|
187
|
+
|
|
188
|
+
<SkillFixDrawer
|
|
189
|
+
:skill="skillDrawerSkill"
|
|
190
|
+
:open="skillDrawerOpen"
|
|
191
|
+
@close="closeSkillDrawer"
|
|
192
|
+
@rolled="refresh"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
196
|
+
|
|
197
|
+
<script setup lang="ts">
|
|
198
|
+
import type { SkillFixEvent } from "~/composables/useStatus";
|
|
199
|
+
|
|
200
|
+
const {
|
|
201
|
+
skillFixes,
|
|
202
|
+
skillFixStats,
|
|
203
|
+
loadSkillFixes,
|
|
204
|
+
loadSkillFixStats,
|
|
205
|
+
startPolling: startSkillPolling
|
|
206
|
+
} = useSkillFixes();
|
|
207
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
208
|
+
const { formatRelative } = useHelpers();
|
|
209
|
+
|
|
210
|
+
const loading = ref(true);
|
|
211
|
+
|
|
212
|
+
const ACTIVE_FIX_STALE_MS = 5 * 60 * 1000;
|
|
213
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
214
|
+
const activeSkillFixes = computed(() =>
|
|
215
|
+
(skillFixes.value as SkillFixEvent[]).filter(
|
|
216
|
+
(e) => e.status === "generating" && Date.now() - new Date(e.ts).getTime() < ACTIVE_FIX_STALE_MS
|
|
217
|
+
)
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
221
|
+
const improvementEvents = computed(() =>
|
|
222
|
+
(skillFixes.value as SkillFixEvent[]).filter(
|
|
223
|
+
(e) => e.mode === "improve" && e.errorType === "skill-improvement-opportunity"
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
228
|
+
const skillSuccessLabel = computed(() => {
|
|
229
|
+
const r = skillFixStats.value?.successRate24h;
|
|
230
|
+
if (r === null || r === undefined) return "";
|
|
231
|
+
return `${Math.round(r * 100)}% ✓`;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
235
|
+
const skillSuccessPct = computed(() => {
|
|
236
|
+
const r = skillFixStats.value?.successRate24h;
|
|
237
|
+
if (r === null || r === undefined) return 0;
|
|
238
|
+
return Math.round(r * 100);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
242
|
+
const subLine = computed(() => {
|
|
243
|
+
const total = skillFixStats.value?.totalEvents24h ?? 0;
|
|
244
|
+
return total ? `${total} events in last 24h` : "Monitor & retry self-repair attempts";
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
248
|
+
function topFailBarPct(count: number): number {
|
|
249
|
+
const max = skillFixStats.value?.topFailingSkills[0]?.errorCount || 1;
|
|
250
|
+
return Math.min(100, (count / Math.max(max, 1)) * 100);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const ERROR_STATUSES = new Set(["error", "no-fix", "generation-error", "patch-failed"]);
|
|
254
|
+
const SUCCESS_STATUSES = new Set(["pr-created", "patched", "patched-with-backup"]);
|
|
255
|
+
|
|
256
|
+
interface SkillGroup {
|
|
257
|
+
skill: string;
|
|
258
|
+
total: number;
|
|
259
|
+
errors: number;
|
|
260
|
+
success: number;
|
|
261
|
+
location?: string;
|
|
262
|
+
lastTs: string;
|
|
263
|
+
lastStatus: SkillFixEvent["status"];
|
|
264
|
+
lastError?: string;
|
|
265
|
+
lastPrUrl?: string;
|
|
266
|
+
lastFiles?: number;
|
|
267
|
+
events: SkillFixEvent[];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const groups = computed<SkillGroup[]>(() => {
|
|
271
|
+
const map = new Map<string, SkillGroup>();
|
|
272
|
+
const sorted = [...(skillFixes.value as SkillFixEvent[])].sort(
|
|
273
|
+
(a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime()
|
|
274
|
+
);
|
|
275
|
+
for (const ev of sorted) {
|
|
276
|
+
let g = map.get(ev.skill);
|
|
277
|
+
if (!g) {
|
|
278
|
+
g = {
|
|
279
|
+
skill: ev.skill,
|
|
280
|
+
total: 0,
|
|
281
|
+
errors: 0,
|
|
282
|
+
success: 0,
|
|
283
|
+
location: ev.location,
|
|
284
|
+
lastTs: ev.ts,
|
|
285
|
+
lastStatus: ev.status,
|
|
286
|
+
lastError: ev.error,
|
|
287
|
+
lastPrUrl: ev.prUrl,
|
|
288
|
+
lastFiles: ev.files,
|
|
289
|
+
events: []
|
|
290
|
+
};
|
|
291
|
+
map.set(ev.skill, g);
|
|
292
|
+
}
|
|
293
|
+
g.total++;
|
|
294
|
+
if (ERROR_STATUSES.has(ev.status)) g.errors++;
|
|
295
|
+
if (SUCCESS_STATUSES.has(ev.status)) g.success++;
|
|
296
|
+
g.events.push(ev);
|
|
297
|
+
}
|
|
298
|
+
return [...map.values()];
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const filterKw = ref("");
|
|
302
|
+
const sort = ref<{ col: "skill" | "total" | "errors" | "success" | "lastTs"; dir: "asc" | "desc" }>({
|
|
303
|
+
col: "lastTs",
|
|
304
|
+
dir: "desc"
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
308
|
+
function toggleSort(col: "skill" | "total" | "errors" | "success" | "lastTs") {
|
|
309
|
+
sort.value =
|
|
310
|
+
sort.value.col === col
|
|
311
|
+
? { col, dir: sort.value.dir === "desc" ? "asc" : "desc" }
|
|
312
|
+
: { col, dir: col === "skill" ? "asc" : "desc" };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
316
|
+
function sortIco(col: string): string {
|
|
317
|
+
if (sort.value.col !== col) return "↕";
|
|
318
|
+
return sort.value.dir === "desc" ? "↓" : "↑";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
322
|
+
const filteredGroups = computed(() => {
|
|
323
|
+
const kw = filterKw.value.trim().toLowerCase();
|
|
324
|
+
let list = groups.value;
|
|
325
|
+
if (kw) list = list.filter((g) => g.skill.toLowerCase().includes(kw));
|
|
326
|
+
const { col, dir } = sort.value;
|
|
327
|
+
return [...list].sort((a, b) => {
|
|
328
|
+
let av: number | string;
|
|
329
|
+
let bv: number | string;
|
|
330
|
+
if (col === "lastTs") {
|
|
331
|
+
av = new Date(a.lastTs).getTime();
|
|
332
|
+
bv = new Date(b.lastTs).getTime();
|
|
333
|
+
} else if (col === "skill") {
|
|
334
|
+
av = a.skill;
|
|
335
|
+
bv = b.skill;
|
|
336
|
+
} else {
|
|
337
|
+
av = a[col];
|
|
338
|
+
bv = b[col];
|
|
339
|
+
}
|
|
340
|
+
if (typeof av === "number" && typeof bv === "number") return dir === "asc" ? av - bv : bv - av;
|
|
341
|
+
return dir === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av));
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const expanded = ref(new Set<string>());
|
|
346
|
+
|
|
347
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
348
|
+
function toggleExpand(skill: string) {
|
|
349
|
+
const next = new Set(expanded.value);
|
|
350
|
+
if (next.has(skill)) next.delete(skill);
|
|
351
|
+
else next.add(skill);
|
|
352
|
+
expanded.value = next;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
356
|
+
function skillFixStatusClass(status: SkillFixEvent["status"]): string {
|
|
357
|
+
if (status === "pr-created" || status === "patched" || status === "patched-with-backup") return "green";
|
|
358
|
+
if (status === "generating") return "yellow";
|
|
359
|
+
if (status === "patch-failed") return "orange";
|
|
360
|
+
if (status === "cooldown") return "violet";
|
|
361
|
+
if (status === "error" || status === "no-fix" || status === "generation-error") return "red";
|
|
362
|
+
if (status === "no-op" || status === "pr-exists") return "sky";
|
|
363
|
+
if (status === "lock-skipped" || status === "not-found" || status === "scope-excluded") return "violet";
|
|
364
|
+
return "";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
368
|
+
function skillFixTriggerLabel(errorType?: string): string {
|
|
369
|
+
if (!errorType) return "—";
|
|
370
|
+
if (errorType === "user-feedback") return "user feedback";
|
|
371
|
+
if (errorType === "bash-error-in-skill") return "bash error";
|
|
372
|
+
if (errorType === "tool-error-in-skill") return "mcp error";
|
|
373
|
+
if (errorType === "skill-load-failed") return "load failed";
|
|
374
|
+
if (errorType === "skill-bypassed") return "bypassed";
|
|
375
|
+
if (errorType === "skill-caused-retry") return "silent retry";
|
|
376
|
+
if (errorType === "skill-caused-fallback") return "silent fallback";
|
|
377
|
+
if (errorType === "skill-improvement-opportunity") return "improve";
|
|
378
|
+
return errorType;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const skillDrawerOpen = ref(false);
|
|
382
|
+
const skillDrawerSkill = ref<string | null>(null);
|
|
383
|
+
|
|
384
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
385
|
+
function openSkillDrawer(skill: string) {
|
|
386
|
+
skillDrawerSkill.value = skill;
|
|
387
|
+
skillDrawerOpen.value = true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
391
|
+
function closeSkillDrawer() {
|
|
392
|
+
skillDrawerOpen.value = false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function refresh() {
|
|
396
|
+
loading.value = true;
|
|
397
|
+
await Promise.all([loadSkillFixes(), loadSkillFixStats()]);
|
|
398
|
+
loading.value = false;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
onMounted(async () => {
|
|
402
|
+
startSkillPolling(15000);
|
|
403
|
+
await refresh();
|
|
404
|
+
});
|
|
405
|
+
</script>
|
|
406
|
+
|
|
407
|
+
<style scoped>
|
|
408
|
+
.sort-btn {
|
|
409
|
+
background: none;
|
|
410
|
+
border: none;
|
|
411
|
+
color: var(--ink-3);
|
|
412
|
+
font-size: .72rem;
|
|
413
|
+
font-weight: 600;
|
|
414
|
+
text-transform: uppercase;
|
|
415
|
+
letter-spacing: .5px;
|
|
416
|
+
cursor: pointer;
|
|
417
|
+
padding: 0;
|
|
418
|
+
display: inline-flex;
|
|
419
|
+
align-items: center;
|
|
420
|
+
gap: 3px;
|
|
421
|
+
white-space: nowrap;
|
|
422
|
+
transition: color .15s;
|
|
423
|
+
}
|
|
424
|
+
.sort-btn:hover { color: var(--ink); }
|
|
425
|
+
.sort-active { color: var(--brand) !important; }
|
|
426
|
+
|
|
427
|
+
.fsel {
|
|
428
|
+
height: 36px;
|
|
429
|
+
padding: 0 8px;
|
|
430
|
+
border-radius: 6px;
|
|
431
|
+
border: 1px solid var(--border-color);
|
|
432
|
+
background: var(--bg);
|
|
433
|
+
color: var(--ink);
|
|
434
|
+
font-size: .82rem;
|
|
435
|
+
outline: none;
|
|
436
|
+
}
|
|
437
|
+
.fsel:focus { outline: 2px solid var(--brand); outline-offset: 1px; }
|
|
438
|
+
|
|
439
|
+
.topfail-row { display: flex; align-items: center; gap: 8px; font-size: .78rem; }
|
|
440
|
+
.topfail-name {
|
|
441
|
+
font-family: var(--f-mono);
|
|
442
|
+
font-weight: 600;
|
|
443
|
+
color: var(--ink-1);
|
|
444
|
+
cursor: pointer;
|
|
445
|
+
text-decoration: underline;
|
|
446
|
+
min-width: 140px;
|
|
447
|
+
max-width: 280px;
|
|
448
|
+
overflow: hidden;
|
|
449
|
+
text-overflow: ellipsis;
|
|
450
|
+
white-space: nowrap;
|
|
451
|
+
}
|
|
452
|
+
.topfail-bar { flex: 1; height: 6px; background: var(--bg-input); border-radius: 3px; overflow: hidden; min-width: 60px; }
|
|
453
|
+
.topfail-bar-fill { height: 100%; background: var(--red); transition: width .2s; }
|
|
454
|
+
.topfail-count { font-family: var(--f-mono); font-size: .72rem; color: var(--red); min-width: 50px; text-align: right; }
|
|
455
|
+
.topfail-fixes { font-family: var(--f-mono); font-size: .72rem; color: var(--green); }
|
|
456
|
+
|
|
457
|
+
.expanded-row td { background: var(--bg-alt); }
|
|
458
|
+
|
|
459
|
+
.ev-line {
|
|
460
|
+
padding: 6px 8px;
|
|
461
|
+
background: var(--bg);
|
|
462
|
+
border: 1px solid var(--border-color);
|
|
463
|
+
border-radius: 5px;
|
|
464
|
+
}
|
|
465
|
+
.ev-line-head {
|
|
466
|
+
display: flex;
|
|
467
|
+
align-items: center;
|
|
468
|
+
gap: 6px;
|
|
469
|
+
flex-wrap: wrap;
|
|
470
|
+
}
|
|
471
|
+
.ev-line-error {
|
|
472
|
+
margin-top: 4px;
|
|
473
|
+
font-size: .72rem;
|
|
474
|
+
color: var(--red);
|
|
475
|
+
font-family: var(--f-mono);
|
|
476
|
+
word-break: break-word;
|
|
477
|
+
white-space: pre-wrap;
|
|
478
|
+
background: rgba(239,68,68,.06);
|
|
479
|
+
padding: 4px 6px;
|
|
480
|
+
border-radius: 4px;
|
|
481
|
+
}
|
|
482
|
+
</style>
|