From f5d845ff3192aaeb55730cb1838755c327368c59 Mon Sep 17 00:00:00 2001 From: matevip Date: Mon, 4 May 2026 14:00:39 +0800 Subject: [PATCH] feat(backstage): tool chip, triage sort, bento metadata tiles --- .../backstage/BackstageFocusPanel.vue | 163 ++++++++++++------ .../src/composables/useBackstageAgent.ts | 12 +- mateclaw-ui/src/i18n/locales/en-US.ts | 4 +- mateclaw-ui/src/i18n/locales/zh-CN.ts | 4 +- mateclaw-ui/src/views/Backstage.vue | 100 +++++++++-- 5 files changed, 217 insertions(+), 66 deletions(-) diff --git a/mateclaw-ui/src/components/backstage/BackstageFocusPanel.vue b/mateclaw-ui/src/components/backstage/BackstageFocusPanel.vue index 97d78e65..af492e94 100644 --- a/mateclaw-ui/src/components/backstage/BackstageFocusPanel.vue +++ b/mateclaw-ui/src/components/backstage/BackstageFocusPanel.vue @@ -32,8 +32,13 @@
@{{ run.username }}
- +

{{ humanSentence(run) }}

+
+ {{ run.runningToolName }} +
@@ -61,26 +66,30 @@
- -
-
-
{{ t('backstage.detail.lastHeard') }}
-
{{ formatAge(run.msSinceLastEvent) }} {{ t('backstage.detail.ago') }}
+ +
+
+
{{ formatAge(run.msSinceLastEvent) }}
+
{{ t('backstage.detail.lastHeard') }}
-
-
{{ t('backstage.detail.audience') }}
-
- - {{ t('backstage.detail.noOneListening') }} - - {{ t('backstage.detail.peopleListening', { n: run.subscriberCount }) }} -
+
+
{{ run.subscriberCount }}
+
{{ t('backstage.detail.audience') }}
-
-
{{ t('backstage.detail.session') }}
-
#{{ shortId(run.conversationId) }}
+
+
#{{ shortId(run.conversationId) }}
+
{{ t('backstage.detail.session') }}
-
+
@@ -321,7 +330,7 @@ html.dark .focus-backdrop { color: var(--mc-text-tertiary); } -/* ===== The one sentence ===== */ +/* ===== The one sentence + tool chip ===== */ .focus-saying { font-size: 22px; line-height: 1.45; @@ -329,7 +338,37 @@ html.dark .focus-backdrop { letter-spacing: -0.015em; color: var(--mc-text-primary); text-align: center; - margin: 0 0 24px; + margin: 0 0 12px; +} + +.focus-tool-row { + display: flex; + justify-content: center; + margin-bottom: 24px; +} + +.focus-tool-chip { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: 8px; + background: var(--mc-accent-soft); + color: var(--mc-accent); + font-family: ui-monospace, SFMono-Regular, 'JetBrains Mono', Menlo, Consolas, monospace; + font-size: 13px; + font-weight: 500; + letter-spacing: 0.01em; + border: 1px solid transparent; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +html.dark .focus-tool-chip { + background: rgba(92, 166, 157, 0.14); + color: hsl(170, 35%, 72%); + border-color: rgba(92, 166, 157, 0.2); } .focus-callout { @@ -467,49 +506,75 @@ html.dark .time-ring-warn::before { font-weight: 500; } -/* ===== Secondary grid ===== */ -.focus-grid { - margin: 0 0 8px; - border-top: 1px solid var(--mc-border-light); +/* ===== Secondary metadata as bento KPI tiles ===== + * Three equal tiles: each is one glance — big value (mono tabular-nums), + * small caption beneath. Replaces the dt/dd list form, which buried the + * value behind a label and made the section read like a settings panel + * instead of a dashboard. + */ +.focus-tiles { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + margin-bottom: 8px; } -.focus-row { +.focus-tile { + padding: 14px 10px 12px; + background: var(--mc-bg-muted); + border: 1px solid var(--mc-border-light); + border-radius: 12px; display: flex; - justify-content: space-between; - align-items: baseline; - padding: 13px 0; - border-bottom: 1px solid var(--mc-border-light); - margin: 0; + flex-direction: column; + align-items: center; + text-align: center; + min-width: 0; } -.focus-row dt { - font-size: 12px; - color: var(--mc-text-tertiary); - margin: 0; - letter-spacing: 0.01em; +html.dark .focus-tile { + background: rgba(255, 255, 255, 0.04); } -.focus-row dd { - font-size: 13.5px; +.focus-tile-value { + font-size: 20px; + font-weight: 600; + letter-spacing: -0.02em; color: var(--mc-text-primary); - margin: 0; - text-align: right; font-variant-numeric: tabular-nums; + line-height: 1.1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; } -.focus-warn { - color: hsl(265, 50%, 50%); -} - -html.dark .focus-warn { - color: hsl(265, 55%, 70%); -} - -.focus-id { +.focus-tile-id { font-family: ui-monospace, SFMono-Regular, 'JetBrains Mono', Menlo, Consolas, monospace; - font-size: 12px; + font-size: 14px; letter-spacing: 0.02em; + color: var(--mc-text-secondary); + font-weight: 500; + cursor: default; +} + +.focus-tile-warn { + color: hsl(265, 50%, 52%); +} + +html.dark .focus-tile-warn { + color: hsl(265, 55%, 72%); +} + +.focus-tile-label { + font-size: 10.5px; color: var(--mc-text-tertiary); + letter-spacing: 0.06em; + margin-top: 8px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; } /* ===== Subagents ===== */ diff --git a/mateclaw-ui/src/composables/useBackstageAgent.ts b/mateclaw-ui/src/composables/useBackstageAgent.ts index ed1fe25c..6a6a5f69 100644 --- a/mateclaw-ui/src/composables/useBackstageAgent.ts +++ b/mateclaw-ui/src/composables/useBackstageAgent.ts @@ -61,12 +61,20 @@ export function useBackstageAgent() { return t('backstage.dotTitle.healthy') } + /** + * One-sentence status. Deliberately does NOT inline the tool name — + * the tool deserves its own monospace chip in the UI, and embedding + * it inside a sentence both gets ellipsis-truncated on narrow cards + * AND duplicates information when the chip is also rendered. + * stuckCallout (used in the focus panel) is the one place that still + * mentions the tool name, because there it lives in a long-form alert. + */ function humanSentence(run: BackstageRunCard): string { - if (run.stuckReason === 'tool_silent') return t('backstage.saying.toolSilent', { tool: run.runningToolName || t('backstage.aTool') }) + if (run.stuckReason === 'tool_silent') return t('backstage.saying.toolSilentBare') if (run.stuckReason === 'idle_silent') return t('backstage.saying.idleSilent') if (run.stuckReason === 'hard_cap') return t('backstage.saying.hardCap') if (run.currentPhase === 'awaiting_approval') return t('backstage.saying.awaitingApproval') - if (run.runningToolName) return t('backstage.saying.usingTool', { tool: run.runningToolName }) + if (run.runningToolName) return t('backstage.saying.usingToolBare') if (run.currentPhase === 'executing_tool') return t('backstage.saying.usingSomething') if (run.currentPhase === 'summarizing') return t('backstage.saying.wrappingUp') if (run.currentPhase === 'planning') return t('backstage.saying.planning') diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index fd685a52..633af86d 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -346,10 +346,10 @@ export default { replying: 'Writing the reply...', planning: 'Planning the next steps...', summarizing: 'Wrapping up...', - usingTool: 'Using {tool}...', + usingToolBare: 'Running', usingSomething: 'Using a tool...', awaitingApproval: 'Waiting for your approval.', - toolSilent: 'Stuck on {tool}, no progress.', + toolSilentBare: 'Stuck on a tool, no progress.', idleSilent: 'Quiet for too long.', hardCap: 'Has been at this far too long.', }, diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index daf87e14..1f302404 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -1526,10 +1526,10 @@ export default { replying: '正在写回复...', planning: '正在规划下一步...', summarizing: '正在收尾...', - usingTool: '正在使用 {tool}...', + usingToolBare: '正在使用', usingSomething: '正在使用某个工具...', awaitingApproval: '在等你审批。', - toolSilent: '卡在 {tool},没有进展。', + toolSilentBare: '卡在工具上,没有进展。', idleSilent: '安静太久了。', hardCap: '已经跑了太久。', }, diff --git a/mateclaw-ui/src/views/Backstage.vue b/mateclaw-ui/src/views/Backstage.vue index 5fb382c2..3a7f96a1 100644 --- a/mateclaw-ui/src/views/Backstage.vue +++ b/mateclaw-ui/src/views/Backstage.vue @@ -99,8 +99,14 @@
- -
{{ humanSentence(run) }}
+ +
+ {{ humanSentence(run) }} + + {{ run.runningToolName }} + +
@@ -257,14 +263,37 @@ const showFilterRow = computed(() => { return s.stuck > 0 || s.orphan > 0 }) +/** + * Triage order: stuck first (you have to do something), then orphan (a run + * that nobody is watching), then working (healthy & boring). Within each + * tier, the older run wins — a 5-minute stuck run is more urgent than one + * stuck for 8 seconds. + * + * Without this, runs are listed in whatever order the snapshot returned + * them, and the one card you actually need to look at can be 4 rows down. + */ +function tierOf(r: BackstageRunCard): number { + if (r.stuckReason) return 0 + if (r.orphan) return 1 + return 2 +} + const visibleRuns = computed(() => { const runs = snapshot.value?.runs ?? [] - switch (activeFilter.value) { - case 'working': return runs.filter(isWorking) - case 'attention': return runs.filter(isAttention) - case 'quiet': return runs.filter(isQuiet) - default: return runs - } + const filtered = (() => { + switch (activeFilter.value) { + case 'working': return runs.filter(isWorking) + case 'attention': return runs.filter(isAttention) + case 'quiet': return runs.filter(isQuiet) + default: return runs + } + })() + return [...filtered].sort((a, b) => { + const ta = tierOf(a) + const tb = tierOf(b) + if (ta !== tb) return ta - tb + return b.ageMs - a.ageMs + }) }) // If the filter the user picked no longer matches any rows (e.g., the stuck @@ -867,13 +896,20 @@ html.dark .agent-avatar { 50% { opacity: 0.7; transform: scale(0.85); } } -/* ===== Saying ===== */ +/* ===== Saying + tool chip ===== */ +.agent-saying-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-bottom: 16px; + min-height: 22px; +} + .agent-saying { font-size: 14.5px; line-height: 1.55; color: var(--mc-text-secondary); - min-height: 22px; - margin-bottom: 16px; letter-spacing: -0.005em; } @@ -886,6 +922,48 @@ html.dark .agent-card.is-stuck .agent-saying { color: hsl(28, 80%, 76%); } +/* + * Tool chip — monospace, accent-soft tinted. The tool name was previously + * baked into the sentence and got truncated on narrow cards; promoting it + * to its own chip makes it ellipsis-resistant and visually weighty. It also + * stops competing with the sentence for the same line of attention. + */ +.tool-chip { + display: inline-flex; + align-items: center; + padding: 2px 9px; + border-radius: 6px; + background: var(--mc-accent-soft); + color: var(--mc-accent); + font-family: ui-monospace, SFMono-Regular, 'JetBrains Mono', Menlo, Consolas, monospace; + font-size: 11.5px; + font-weight: 500; + letter-spacing: 0.01em; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border: 1px solid transparent; +} + +html.dark .tool-chip { + background: rgba(92, 166, 157, 0.14); + color: hsl(170, 35%, 70%); + border-color: rgba(92, 166, 157, 0.18); +} + +.agent-card.is-stuck .tool-chip { + background: hsla(20, 100%, 90%, 0.65); + color: hsl(20, 75%, 38%); + border-color: hsla(20, 80%, 55%, 0.25); +} + +html.dark .agent-card.is-stuck .tool-chip { + background: hsla(20, 60%, 30%, 0.4); + color: hsl(28, 80%, 78%); + border-color: hsla(20, 70%, 55%, 0.3); +} + /* ===== Meta + bar ===== */ .agent-meta-row { display: flex;