mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(backstage): tool chip, triage sort, bento metadata tiles
This commit is contained in:
parent
ce5741c45f
commit
f5d845ff31
@ -32,8 +32,13 @@
|
||||
<div class="focus-subtitle" v-if="run.username">@{{ run.username }}</div>
|
||||
</div>
|
||||
|
||||
<!-- The one sentence: what it's doing right now -->
|
||||
<!-- The one sentence: what it's doing right now. The tool name,
|
||||
when present, sits below as its own chip — sentence stays
|
||||
short and readable; the tool gets monospace prominence. -->
|
||||
<p class="focus-saying">{{ humanSentence(run) }}</p>
|
||||
<div v-if="run.runningToolName" class="focus-tool-row">
|
||||
<span class="focus-tool-chip" :title="run.runningToolName">{{ run.runningToolName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Stuck callout -->
|
||||
<div v-if="run.stuckReason" class="focus-callout">
|
||||
@ -61,26 +66,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary metadata -->
|
||||
<dl class="focus-grid">
|
||||
<div class="focus-row">
|
||||
<dt>{{ t('backstage.detail.lastHeard') }}</dt>
|
||||
<dd>{{ formatAge(run.msSinceLastEvent) }} {{ t('backstage.detail.ago') }}</dd>
|
||||
<!-- Secondary metadata as bento KPI tiles. Each tile is one
|
||||
glance: big value, small caption. The third tile carries
|
||||
the conversation id (full id on hover); the previous list
|
||||
form buried these behind label text. -->
|
||||
<div class="focus-tiles">
|
||||
<div class="focus-tile">
|
||||
<div class="focus-tile-value">{{ formatAge(run.msSinceLastEvent) }}</div>
|
||||
<div class="focus-tile-label">{{ t('backstage.detail.lastHeard') }}</div>
|
||||
</div>
|
||||
<div class="focus-row">
|
||||
<dt>{{ t('backstage.detail.audience') }}</dt>
|
||||
<dd>
|
||||
<span v-if="run.subscriberCount === 0" class="focus-warn">
|
||||
{{ t('backstage.detail.noOneListening') }}
|
||||
</span>
|
||||
<span v-else>{{ t('backstage.detail.peopleListening', { n: run.subscriberCount }) }}</span>
|
||||
</dd>
|
||||
<div class="focus-tile">
|
||||
<div
|
||||
class="focus-tile-value"
|
||||
:class="{ 'focus-tile-warn': run.subscriberCount === 0 }"
|
||||
>{{ run.subscriberCount }}</div>
|
||||
<div class="focus-tile-label">{{ t('backstage.detail.audience') }}</div>
|
||||
</div>
|
||||
<div class="focus-row">
|
||||
<dt>{{ t('backstage.detail.session') }}</dt>
|
||||
<dd class="focus-id" :title="run.conversationId">#{{ shortId(run.conversationId) }}</dd>
|
||||
<div class="focus-tile">
|
||||
<div
|
||||
class="focus-tile-value focus-tile-id"
|
||||
:title="run.conversationId"
|
||||
>#{{ shortId(run.conversationId) }}</div>
|
||||
<div class="focus-tile-label">{{ t('backstage.detail.session') }}</div>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Helpers (subagents) -->
|
||||
<div v-if="subagents.length > 0" class="focus-section">
|
||||
@ -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 ===== */
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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.',
|
||||
},
|
||||
|
||||
@ -1526,10 +1526,10 @@ export default {
|
||||
replying: '正在写回复...',
|
||||
planning: '正在规划下一步...',
|
||||
summarizing: '正在收尾...',
|
||||
usingTool: '正在使用 {tool}...',
|
||||
usingToolBare: '正在使用',
|
||||
usingSomething: '正在使用某个工具...',
|
||||
awaitingApproval: '在等你审批。',
|
||||
toolSilent: '卡在 {tool},没有进展。',
|
||||
toolSilentBare: '卡在工具上,没有进展。',
|
||||
idleSilent: '安静太久了。',
|
||||
hardCap: '已经跑了太久。',
|
||||
},
|
||||
|
||||
@ -99,8 +99,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Single human sentence -->
|
||||
<div class="agent-saying">{{ humanSentence(run) }}</div>
|
||||
<!-- Status sentence + (when present) the tool chip standing
|
||||
on its own so a long tool name doesn't get ellipsis-eaten. -->
|
||||
<div class="agent-saying-row">
|
||||
<span class="agent-saying">{{ humanSentence(run) }}</span>
|
||||
<span v-if="run.runningToolName" class="tool-chip" :title="run.runningToolName">
|
||||
{{ run.runningToolName }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Meta: just the time + orphan hint (id moved to detail) -->
|
||||
<div class="agent-meta-row">
|
||||
@ -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<BackstageRunCard[]>(() => {
|
||||
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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user