diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts
index a83c29fa..f7b15bea 100644
--- a/mateclaw-ui/src/i18n/locales/en-US.ts
+++ b/mateclaw-ui/src/i18n/locales/en-US.ts
@@ -2373,22 +2373,13 @@ export default {
triggers: {
kicker: 'Triggers',
title: 'Workflow Triggers',
- desc: 'Connect cron schedules and channel events to your published workflows. pattern_version increments on every cron expression change so multi-node deployments coordinate correctly.',
+ desc: 'Connect cron schedules, channel events and webhooks to your published workflows — the workflow runs automatically when the condition is met.',
newTrigger: '+ New Trigger',
- columns: {
- name: 'Name',
- pattern: 'Pattern',
- target: 'Target Workflow',
- rate: 'Rate',
- fires: 'Fires',
- patternVersion: 'pattern_v',
- lastFired: 'Last fired',
- state: 'State',
- actions: 'Actions',
- },
+ emptyDesc: 'Triggers run a workflow automatically when a specific event happens — no manual start needed.',
+ neverFired: 'Never fired',
+ firedSummary: 'Fired {count}× · last {time}',
targetWorkflowSelect: 'Pick a published workflow',
targetWorkflowEmpty: 'No published workflows yet.',
- lastDispatchedAt: 'Last attempt',
patternTypeLabels: {
cron: 'Cron schedule',
channel_message: 'Channel message',
@@ -2440,10 +2431,9 @@ export default {
errSubstringRequired: 'Substring cannot be empty or the trigger never fires',
},
unnamed: '(unnamed)',
- rateUnit: '{count}/min',
enabled: 'enabled',
disabled: 'disabled',
- empty: 'No triggers configured.',
+ empty: 'No triggers yet',
actions: {
edit: 'Edit',
delete: 'Delete',
diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts
index 4ca1f6e6..9785285c 100644
--- a/mateclaw-ui/src/i18n/locales/zh-CN.ts
+++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts
@@ -2385,22 +2385,13 @@ export default {
triggers: {
kicker: '触发器',
title: '工作流触发器',
- desc: '把 cron 调度和渠道事件接到已发布的工作流上。每次修改 cron 表达式都会让 pattern_version 自增,多节点部署据此协调。',
+ desc: '把定时调度、渠道事件和 Webhook 接到已发布的工作流上——条件满足时,工作流自动运行。',
newTrigger: '+ 新建触发器',
- columns: {
- name: '名称',
- pattern: '模式',
- target: '目标工作流',
- rate: '速率',
- fires: '触发次数',
- patternVersion: 'pattern_v',
- lastFired: '上次触发',
- state: '状态',
- actions: '操作',
- },
+ emptyDesc: '触发器让工作流在特定事件发生时自动运行,无需手动启动。',
+ neverFired: '尚未触发',
+ firedSummary: '已触发 {count} 次 · 最近 {time}',
targetWorkflowSelect: '选择已发布的工作流',
targetWorkflowEmpty: '尚无已发布的工作流。',
- lastDispatchedAt: '最近一次尝试',
patternTypeLabels: {
cron: '定时任务',
channel_message: '渠道消息',
@@ -2452,10 +2443,9 @@ export default {
errSubstringRequired: '子串不能为空,否则触发器不会激活',
},
unnamed: '(未命名)',
- rateUnit: '{count}/分钟',
enabled: '已启用',
disabled: '已停用',
- empty: '暂无触发器配置。',
+ empty: '还没有触发器',
actions: {
edit: '编辑',
delete: '删除',
diff --git a/mateclaw-ui/src/views/Triggers.vue b/mateclaw-ui/src/views/Triggers.vue
index a0592e6b..f6e012b5 100644
--- a/mateclaw-ui/src/views/Triggers.vue
+++ b/mateclaw-ui/src/views/Triggers.vue
@@ -11,60 +11,77 @@
-
-
-
- | {{ t('triggers.columns.name') }} |
- {{ t('triggers.columns.pattern') }} |
- {{ t('triggers.columns.target') }} |
- {{ t('triggers.columns.rate') }} |
- {{ t('triggers.columns.fires') }} |
- {{ t('triggers.columns.patternVersion') }} |
- {{ t('triggers.columns.lastFired') }} |
- {{ t('triggers.columns.state') }} |
- {{ t('triggers.columns.actions') }} |
-
-
-
-
- |
- {{ row.name || t('triggers.unnamed') }}
-
+
+
+ {{ t('triggers.empty') }}
+ {{ t('triggers.emptyDesc') }}
+
+
+
+
+
+
+
+
+ {{ row.name || t('triggers.unnamed') }}
+
⚠ {{ truncateError(row.lastError) }}
-
- |
-
- {{ patternTypeLabel(row.patternType) }}
- {{ patternSummary(row) }}
- |
- {{ formatTarget(row) }} |
- {{ t('triggers.rateUnit', { count: row.rateLimitPerMin }) }} |
- {{ row.fireCount }} / {{ row.maxFires }} |
- {{ row.patternVersion }} |
-
- {{ formatTime(row.lastFiredAt) }}
-
- {{ formatTime(row.lastDispatchedAt) }}
-
- |
-
-
- |
-
-
-
- |
-
-
- | {{ t('triggers.empty') }} |
-
-
-
+
+
+
+
+ {{ patternTypeLabel(row.patternType) }}
+
+ →
+ {{ targetName(row) }}
+
+
+ {{ patternSummary(row) }}
+ ·
+ {{ activityLine(row) }}
+
+
+
+
+
+
+
+
+
@@ -258,17 +275,30 @@ async function reload() {
}
}
-function formatTarget(row: TriggerSummary) {
+function targetName(row: TriggerSummary): string {
if (row.targetType === 'workflow') {
- const wf = workflows.value.find((w) => w.id === row.targetId)
- if (wf?.name) return `${wf.name} (#${row.targetId})`
+ const wf = workflows.value.find((w) => String(w.id) === String(row.targetId))
+ if (wf?.name) return wf.name
}
- return `${row.targetType}#${row.targetId}`
+ return `${row.targetType} #${row.targetId}`
}
-function formatTime(iso?: string) {
+// Compact "MM-DD HH:mm" for the activity subline — the full timestamp would
+// crowd the card without telling the operator anything they scan for.
+function shortTime(iso?: string): string {
if (!iso) return '-'
- return iso.replace('T', ' ').slice(0, 19)
+ return iso.slice(5, 16).replace('T', ' ')
+}
+
+// One muted line summarizing operational state: how many times it has fired
+// and when. Replaces the former fireCount / lastFired / pattern_version
+// columns — those scalars never warranted a column each.
+function activityLine(row: TriggerSummary): string {
+ if (!row.lastFiredAt) return t('triggers.neverFired')
+ return t('triggers.firedSummary', {
+ count: row.fireCount,
+ time: shortTime(row.lastFiredAt),
+ })
}
function patternTypeLabel(pt: string): string {
@@ -409,59 +439,77 @@ watch(workspaceId, reload)
flex-direction: column;
gap: 16px;
}
-.triggers-table {
- width: 100%;
- border-collapse: collapse;
- padding: 0;
- overflow: hidden;
-}
-.triggers-table th,
-.triggers-table td {
- padding: 10px 12px;
- border-bottom: 1px solid var(--mc-border, rgba(0, 0, 0, 0.06));
- text-align: left;
- font-size: 13px;
- vertical-align: top;
-}
-.triggers-table th {
- font-weight: 600;
- background: var(--mc-bg-sunken, rgba(0, 0, 0, 0.04));
-}
-.pattern-detail {
- font-family: 'JetBrains Mono', Consolas, monospace;
- font-size: 11px;
- opacity: 0.7;
- margin-top: 2px;
-}
-.pattern-summary {
- font-family: 'JetBrains Mono', Consolas, monospace;
- font-size: 11px;
- opacity: 0.7;
- margin-top: 3px;
+.mc-page-header .btn-primary {
white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 240px;
+}
+/* ── Trigger list — one card per rule, reads as "when X → run Y" ── */
+.trigger-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.trigger-card {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 13px 18px;
+ transition: opacity 0.15s ease, border-color 0.15s ease;
+}
+.trigger-card.is-disabled {
+ opacity: 0.6;
+}
+.trigger-status-dot {
+ flex: 0 0 auto;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+}
+.trigger-status-dot.is-on {
+ background: #22c55e;
+ box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.16);
+}
+.trigger-status-dot.is-off {
+ background: var(--mc-text-tertiary, #b0a89e);
+}
+.trigger-main {
+ flex: 1 1 auto;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+.trigger-headline {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
}
.trigger-name {
- font-weight: 500;
-}
-.trigger-last-error {
- font-size: 11px;
- color: var(--mc-danger, #c0392b);
- margin-top: 2px;
+ font-weight: 600;
+ font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- max-width: 240px;
+}
+.trigger-error-chip {
+ flex: 0 0 auto;
+ font-size: 11px;
+ color: var(--mc-danger, #c0392b);
+ background: var(--mc-danger-bg, rgba(192, 57, 43, 0.1));
+ padding: 1px 8px;
+ border-radius: 999px;
cursor: help;
}
-.trigger-attempt-time {
- font-size: 10px;
- opacity: 0.55;
- margin-top: 1px;
+/* Line 2 — the rule at its purest: "[type] → [workflow]". Both ends are
+ short, so this never needs to truncate; detail lives on the meta line. */
+.trigger-rule {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
}
.trigger-type-pill {
+ flex: 0 0 auto;
display: inline-block;
padding: 2px 8px;
font-size: 11px;
@@ -473,20 +521,132 @@ watch(workspaceId, reload)
letter-spacing: 0.02em;
}
:lang(zh-CN) .trigger-type-pill { text-transform: none; }
-.empty-row {
- text-align: center;
- opacity: 0.6;
- padding: 24px;
+.trigger-rule-arrow {
+ flex: 0 0 auto;
+ color: var(--mc-text-tertiary, #b0a89e);
+ font-size: 13px;
}
-.toggle {
+.trigger-target {
+ flex: 0 1 auto;
+ font-size: 13px;
+ font-weight: 500;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+/* Line 3 — muted detail: pattern summary + activity. Wraps instead of
+ truncating so neither the schedule nor the fire stats get amputated. */
+.trigger-meta {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 2px 7px;
+ font-size: 11.5px;
+ color: var(--mc-text-tertiary, #9a938b);
+}
+.trigger-meta-summary {
+ font-family: 'JetBrains Mono', Consolas, monospace;
+ font-size: 11px;
+}
+.trigger-meta-sep {
+ opacity: 0.5;
+}
+.trigger-controls {
+ flex: 0 0 auto;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+/* Toggle switch — enabled state is user-controlled, so it earns a switch */
+.trigger-toggle {
+ flex: 0 0 auto;
+ position: relative;
+ width: 38px;
+ height: 22px;
+ padding: 0;
+ border: none;
+ border-radius: 999px;
+ background: var(--mc-bg-sunken, rgba(0, 0, 0, 0.16));
+ cursor: pointer;
+ transition: background 0.18s ease;
+}
+.trigger-toggle.is-on {
+ background: #22c55e;
+}
+.trigger-toggle-knob {
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: #fff;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+ transition: transform 0.18s ease;
+}
+.trigger-toggle.is-on .trigger-toggle-knob {
+ transform: translateX(16px);
+}
+.row-btn {
display: inline-flex;
align-items: center;
- gap: 6px;
- font-size: 12px;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ border: 1px solid var(--mc-border, rgba(0, 0, 0, 0.12));
+ border-radius: 8px;
+ background: var(--mc-bg-elevated, transparent);
+ color: var(--mc-text-secondary, inherit);
+ cursor: pointer;
+ transition: background 0.14s ease, color 0.14s ease, border-color 0.14s ease;
}
-.actions {
+.row-btn svg {
+ width: 15px;
+ height: 15px;
+}
+.row-btn:hover {
+ background: var(--mc-bg-sunken, rgba(0, 0, 0, 0.05));
+ color: var(--mc-text-primary, inherit);
+}
+.row-btn.danger:hover {
+ background: var(--mc-danger-bg, rgba(192, 57, 43, 0.1));
+ border-color: var(--mc-danger, #c0392b);
+ color: var(--mc-danger, #c0392b);
+}
+/* Empty state — a brand-new page should invite, not show a blank table */
+.trigger-empty {
display: flex;
- gap: 6px;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ gap: 10px;
+ padding: 56px 24px;
+}
+.trigger-empty-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 52px;
+ height: 52px;
+ border-radius: 14px;
+ background: var(--mc-bg-muted, rgba(0, 0, 0, 0.06));
+ color: var(--mc-text-tertiary, #9a938b);
+}
+.trigger-empty-icon svg {
+ width: 26px;
+ height: 26px;
+}
+.trigger-empty-title {
+ margin: 4px 0 0;
+ font-size: 15px;
+ font-weight: 600;
+}
+.trigger-empty-desc {
+ margin: 0 0 8px;
+ font-size: 13px;
+ color: var(--mc-text-secondary, inherit);
+ max-width: 380px;
}
.trigger-modal-overlay {
position: fixed;
@@ -624,21 +784,11 @@ button:disabled {
cursor: not-allowed;
}
-/* Mobile / tablet: the trigger table has 9 columns and breaks on
- phones. Wrap it in a horizontal-scroll container, collapse the
- form to one column, and stack the page header. */
-@media (max-width: 900px) {
- .triggers-table {
- display: block;
- overflow-x: auto;
- white-space: nowrap;
- }
- .triggers-table thead,
- .triggers-table tbody,
- .triggers-table tr {
- display: table;
- width: 100%;
- table-layout: auto;
+/* Mobile / tablet: let the rule line wrap instead of truncating, collapse
+ the form to one column, and stack the page header. */
+@media (max-width: 720px) {
+ .trigger-rule {
+ flex-wrap: wrap;
}
.form-grid {
grid-template-columns: 1fr;