mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(goal): checklist UI — progress ring, hover checklist card, criteria SSE
This commit is contained in:
parent
c5ccce8ebc
commit
da332f2fd8
@ -1277,12 +1277,21 @@ export const triggerApi = {
|
||||
}) => http.post('/triggers/events', envelope),
|
||||
}
|
||||
|
||||
// ==================== Persistent goals (RFC 48) ====================
|
||||
// ==================== Persistent goals ====================
|
||||
//
|
||||
// Snowflake IDs are sent as strings end-to-end — the backend's
|
||||
// ToStringSerializer makes responses strings, and request payloads keep
|
||||
// them as strings to dodge JS Number precision loss. See CLAUDE.md
|
||||
// "ID Handling — Snowflake Precision Convention".
|
||||
|
||||
/** One checkable item of a goal's exit checklist. */
|
||||
export interface GoalCriterion {
|
||||
id: string
|
||||
text: string
|
||||
passed: boolean
|
||||
evidence?: string
|
||||
}
|
||||
|
||||
export interface Goal {
|
||||
id: string
|
||||
conversationId: string
|
||||
@ -1298,6 +1307,7 @@ export interface Goal {
|
||||
llmCallBudget: number
|
||||
agentLlmCallsUsed: number
|
||||
evalLlmCallsUsed: number
|
||||
totalLlmCallsUsed?: number
|
||||
progressSummary?: string | null
|
||||
completionScore?: number | null
|
||||
lastEvaluationAt?: string | null
|
||||
@ -1306,6 +1316,8 @@ export interface Goal {
|
||||
lastFollowupAt?: string | null
|
||||
createTime: string
|
||||
updateTime: string
|
||||
/** Parsed checklist; always an array on the wire (empty when none). */
|
||||
criteria?: GoalCriterion[]
|
||||
}
|
||||
|
||||
export interface GoalEvent {
|
||||
@ -1329,6 +1341,7 @@ export const goalApi = {
|
||||
llmCallBudget?: number
|
||||
autoFollowupEnabled?: boolean
|
||||
followupCooldownSeconds?: number
|
||||
criteria?: { text: string }[]
|
||||
}) => http.post<Goal>('/goals', data),
|
||||
|
||||
findActive: (conversationId: string) =>
|
||||
|
||||
@ -69,6 +69,14 @@ const tooltip = computed(() => {
|
||||
}
|
||||
return parts.join(' · ')
|
||||
})
|
||||
|
||||
// Checklist for the richer hover card. Empty until a checklist exists.
|
||||
const criteria = computed(() => goal.value?.criteria ?? [])
|
||||
const progressLabel = computed(() => {
|
||||
if (!props.conversationId) return ''
|
||||
const p = goalStore.criteriaProgress(props.conversationId)
|
||||
return p ? `${p.passed}/${p.total}` : ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -107,7 +115,20 @@ const tooltip = computed(() => {
|
||||
/>
|
||||
</svg>
|
||||
<span v-if="showFollowupMark" class="followup-mark" :title="$t('goal.autoFollowup')">↻</span>
|
||||
<span v-if="goal && tooltip" class="goal-tip">{{ tooltip }}</span>
|
||||
<!-- Checklist card when the goal has criteria; plain one-liner otherwise. -->
|
||||
<div v-if="goal && criteria.length" class="goal-card">
|
||||
<div class="goal-card-head">
|
||||
<span class="goal-card-title">{{ goal.title }}</span>
|
||||
<span v-if="progressLabel" class="goal-card-count">{{ progressLabel }}</span>
|
||||
</div>
|
||||
<ul class="goal-card-list">
|
||||
<li v-for="c in criteria" :key="c.id" :class="{ done: c.passed }">
|
||||
<span class="goal-card-mark">{{ c.passed ? '✓' : '○' }}</span>
|
||||
<span class="goal-card-text">{{ c.text }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<span v-else-if="goal && tooltip" class="goal-tip">{{ tooltip }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -246,4 +267,78 @@ const tooltip = computed(() => {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) translateX(2px);
|
||||
}
|
||||
|
||||
/* Checklist hover card — same reveal mechanics as the tooltip, but a
|
||||
* multi-line block listing each criterion with a done marker. */
|
||||
.goal-card {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: calc(100% + 14px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 280px;
|
||||
background: var(--mc-text-primary, #1d1612);
|
||||
color: var(--mc-bg-elevated, #ffffff);
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.22);
|
||||
transition: opacity 150ms ease, transform 150ms ease;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.avatar-with-ring:hover .goal-card {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) translateX(2px);
|
||||
}
|
||||
.goal-card-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.goal-card-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.goal-card-count {
|
||||
font-size: 11px;
|
||||
color: #b6905b;
|
||||
font-variant-numeric: tabular-nums;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.goal-card-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.goal-card-list li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 7px;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
.goal-card-list li.done .goal-card-text {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.goal-card-mark {
|
||||
flex-shrink: 0;
|
||||
width: 12px;
|
||||
text-align: center;
|
||||
color: #9b7d6c;
|
||||
}
|
||||
.goal-card-list li.done .goal-card-mark {
|
||||
color: #2f8a6d;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -34,6 +34,9 @@ async function accept() {
|
||||
props.agentId,
|
||||
props.workspaceId,
|
||||
props.suggestedTitle,
|
||||
// Default to autonomous continuation when the user opts in from here —
|
||||
// the whole point of accepting is "keep working toward this".
|
||||
{ autoFollowup: true },
|
||||
)
|
||||
} finally {
|
||||
busy.value = false
|
||||
|
||||
@ -50,7 +50,7 @@ export type SSEEventType =
|
||||
| 'delegation_async_spawned'
|
||||
// Heartbeat watchdog flagged a sub-agent as making no observable progress
|
||||
| 'subagent_stale'
|
||||
// Persistent goal events (RFC 48) — emitted by GoalEvaluationNode
|
||||
// Persistent goal events — emitted by GoalEvaluationNode
|
||||
| 'goal_evaluated'
|
||||
| 'goal_followup'
|
||||
| 'goal_completed'
|
||||
|
||||
@ -94,7 +94,7 @@ export const useGoalStore = defineStore('goal', () => {
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
title: string,
|
||||
opts: { description?: string; exitCriteria?: string; autoFollowup?: boolean } = {},
|
||||
opts: { description?: string; exitCriteria?: string; autoFollowup?: boolean; criteria?: string[] } = {},
|
||||
): Promise<Goal | null> {
|
||||
try {
|
||||
const res: any = await goalApi.create({
|
||||
@ -105,6 +105,7 @@ export const useGoalStore = defineStore('goal', () => {
|
||||
description: opts.description,
|
||||
exitCriteria: opts.exitCriteria,
|
||||
autoFollowupEnabled: opts.autoFollowup,
|
||||
criteria: opts.criteria?.map((text) => ({ text })),
|
||||
})
|
||||
const goal: Goal = res?.data
|
||||
activeGoalByConv.value[conversationId] = goal
|
||||
@ -170,11 +171,14 @@ export const useGoalStore = defineStore('goal', () => {
|
||||
case 'goal_evaluated': {
|
||||
evaluatingByConv.value[conversationId] = false
|
||||
lastTerminalEventAtByConv.value[conversationId] = Date.now()
|
||||
if (goal && data?.score != null) {
|
||||
goal.completionScore = Number(data.score)
|
||||
}
|
||||
if (goal && typeof data?.gap === 'string') {
|
||||
goal.progressSummary = data.gap
|
||||
// Prefer the full goal snapshot (carries the criteria array + score);
|
||||
// fall back to patching the cached goal for older payload shapes.
|
||||
const fresh = data?.goal as Goal | undefined
|
||||
if (fresh && typeof fresh.id === 'string') {
|
||||
activeGoalByConv.value[conversationId] = fresh
|
||||
} else if (goal) {
|
||||
if (data?.score != null) goal.completionScore = Number(data.score)
|
||||
if (typeof data?.gap === 'string') goal.progressSummary = data.gap
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -185,6 +189,11 @@ export const useGoalStore = defineStore('goal', () => {
|
||||
// its evaluating state until message_complete fires for that
|
||||
// followup turn — so the user sees breathe → still → breathe.
|
||||
pendingFollowupByConv.value[conversationId] = true
|
||||
// The followup payload carries the latest criteria progress.
|
||||
const fresh = data?.goal as Goal | undefined
|
||||
if (fresh && typeof fresh.id === 'string') {
|
||||
activeGoalByConv.value[conversationId] = fresh
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'goal_completed': {
|
||||
@ -269,10 +278,24 @@ export const useGoalStore = defineStore('goal', () => {
|
||||
|
||||
function progressFraction(conversationId: string): number | null {
|
||||
const g = activeGoal(conversationId)
|
||||
if (!g || g.completionScore == null) return null
|
||||
if (!g) return null
|
||||
// Prefer the deterministic checklist (passed / total) when present;
|
||||
// fall back to the evaluator's completion score otherwise.
|
||||
if (g.criteria && g.criteria.length > 0) {
|
||||
const passed = g.criteria.filter((c) => c.passed).length
|
||||
return passed / g.criteria.length
|
||||
}
|
||||
if (g.completionScore == null) return null
|
||||
return Math.max(0, Math.min(1, g.completionScore))
|
||||
}
|
||||
|
||||
/** Checklist progress as { passed, total } when a checklist exists. */
|
||||
function criteriaProgress(conversationId: string): { passed: number; total: number } | null {
|
||||
const g = activeGoal(conversationId)
|
||||
if (!g || !g.criteria || g.criteria.length === 0) return null
|
||||
return { passed: g.criteria.filter((c) => c.passed).length, total: g.criteria.length }
|
||||
}
|
||||
|
||||
// ==================== Inline prompt + system line helpers ====================
|
||||
|
||||
function isPromptDismissed(conversationId: string): boolean {
|
||||
@ -349,6 +372,7 @@ export const useGoalStore = defineStore('goal', () => {
|
||||
isEvaluating,
|
||||
activeGoal,
|
||||
progressFraction,
|
||||
criteriaProgress,
|
||||
isPromptDismissed,
|
||||
dismissPrompt,
|
||||
clearDismissedPrompt,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user