From 1ae0b248843eafa0faad3b3995c65c32a95ecc1a Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Thu, 1 Oct 2026 21:58:03 +0800 Subject: [PATCH 1/4] refactor(app): name the delegation state behind its label delegationState() returns the typed state that delegationStateLabel() already decided implicitly, so presentation can group records by the same classification. Labels and precedence are unchanged. Signed-off-by: huangruiteng --- apps/presentation/dashboard/src/data/chat.ts | 36 +++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/apps/presentation/dashboard/src/data/chat.ts b/apps/presentation/dashboard/src/data/chat.ts index 277776b4fa..470ae15faf 100644 --- a/apps/presentation/dashboard/src/data/chat.ts +++ b/apps/presentation/dashboard/src/data/chat.ts @@ -1106,16 +1106,34 @@ export function readManagedGoalResult(goalId: string, todoId: string) { `/api/chat/goal-results/${encodeURIComponent(todoId)}?goal_id=${encodeURIComponent(goalId)}`, ); } +export type DelegationState = "unavailable" | "accepted" | "rejected" | "recovery_required" + | "executing" | "validating" | "dispatched" | "unknown"; +type DelegationStateFacts = {status: string; worker_active?: boolean; recovery_required: boolean | null}; // Keep inventory and selected-operation labels consistent; unknown states stay unknown. -export function delegationStateLabel(row: {status: string; worker_active?: boolean; recovery_required: boolean | null}, zh: boolean) { - if (row.status === "unavailable") return zh ? "无法核验" : "Unavailable"; - if (row.status === "accepted") return zh ? "已通过当前验收" : "Currently accepted"; - if (row.status === "rejected") return zh ? "未通过验收" : "Rejected"; - if (row.recovery_required) return zh ? "需要恢复原执行" : "Original execution needs recovery"; - if (row.status === "running" && row.worker_active) return zh ? "执行中" : "Executing"; - if (row.status === "turn_returned" && row.worker_active) return zh ? "正在验收" : "Validating"; - if (["prepared", "running", "turn_returned"].includes(row.status)) return zh ? "已派发,等待执行回读" : "Dispatched; awaiting execution readback"; - return zh ? "状态未知" : "Unknown state"; +// "executing" and "validating" require an active worker observation, never the stored status alone. +export function delegationState(row: DelegationStateFacts): DelegationState { + if (row.status === "unavailable") return "unavailable"; + if (row.status === "accepted") return "accepted"; + if (row.status === "rejected") return "rejected"; + if (row.recovery_required) return "recovery_required"; + if (row.status === "running" && row.worker_active) return "executing"; + if (row.status === "turn_returned" && row.worker_active) return "validating"; + if (["prepared", "running", "turn_returned"].includes(row.status)) return "dispatched"; + return "unknown"; +} +const DELEGATION_STATE_LABELS: Record = { + unavailable: {zh: "无法核验", en: "Unavailable"}, + accepted: {zh: "已通过当前验收", en: "Currently accepted"}, + rejected: {zh: "未通过验收", en: "Rejected"}, + recovery_required: {zh: "需要恢复原执行", en: "Original execution needs recovery"}, + executing: {zh: "执行中", en: "Executing"}, + validating: {zh: "正在验收", en: "Validating"}, + dispatched: {zh: "已派发,等待执行回读", en: "Dispatched; awaiting execution readback"}, + unknown: {zh: "状态未知", en: "Unknown state"}, +}; +export function delegationStateLabel(row: DelegationStateFacts, zh: boolean) { + const label = DELEGATION_STATE_LABELS[delegationState(row)]; + return zh ? label.zh : label.en; } export function fetchLoopXTeamWork(sessionId: string, cursor?: string) { return requestJson(`/api/chat/sessions/${sessionId}/loopx`, { From 2ef04cd9199e2698145ae607d9475c4848bf3472 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Thu, 1 Oct 2026 21:58:03 +0800 Subject: [PATCH 2/4] feat(app): show team execution as a pulse strip and member cards The team panel counts this page's delegation records by typed state, places each record under its bound member's card with that member's launch-check pill, and keeps unattributed records in an explicit Other group. Live treatment appears only for an observed active worker. Existing reads, inspection, paging, evidence and honesty notes are unchanged. Signed-off-by: huangruiteng --- .../personal-workspace/goal-loopx-mode.css | 46 ++++++- .../personal-workspace/goal-team-work.tsx | 118 ++++++++++++++---- .../personal-workspace-browser/loopx-mode.mjs | 14 ++- 3 files changed, 149 insertions(+), 29 deletions(-) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-loopx-mode.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-loopx-mode.css index 294b5046bb..80e00e68ac 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-loopx-mode.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-loopx-mode.css @@ -26,10 +26,48 @@ .goal-loopx-message-mode { display: flex; align-items: center; gap: 8px; margin: 0 0 8px; font-size: 12px; flex-wrap: wrap; } .goal-loopx-message-mode select { font: inherit; color: inherit; min-height: 44px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 6px; background: var(--pw-surface, #fff); padding: 6px; } .goal-team-work ul { list-style: none; padding: 0; margin: 8px 0 24px; } -.goal-team-work li { padding: 12px 0; border-bottom: 1px solid var(--pw-line, #ebebeb); overflow-wrap: anywhere; } -.goal-team-bindings li > div, .goal-team-work-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; } -.goal-team-work code { display: block; font-size: 12px; overflow-wrap: anywhere; white-space: normal; padding-bottom: 8px; } -.goal-team-work summary { cursor: pointer; padding: 12px 0; min-height: 44px; font-size: 12px; color: var(--pw-muted, #666); } +.goal-team-work-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; } +.goal-team-work-actions > span { display: inline-flex; gap: 8px; } +.goal-team-work code { display: block; font: 11.5px/1.5 var(--font-mono, monospace); color: var(--pw-muted, #666); overflow-wrap: anywhere; white-space: normal; padding-bottom: 6px; } +.goal-team-work summary { cursor: pointer; padding: 10px 0 4px; min-height: 32px; font-size: 12px; color: var(--pw-muted, #666); } +.goal-team-work .goal-team-note { margin: 4px 0 16px; } +.goal-team-pulse-block { margin-bottom: 8px; } +.goal-team-pulse-block > p { margin: 8px 0 0; } +.goal-team-work .goal-team-pulse { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); margin: 0; overflow: hidden; border: 1px solid var(--pw-line, #ebebeb); border-radius: 12px; background: var(--pw-surface, #fff); } +.goal-team-pulse li { display: grid; gap: 4px; padding: 12px 14px; box-shadow: 1px 0 0 var(--pw-line, #ebebeb), 0 1px 0 var(--pw-line, #ebebeb); } +.goal-team-pulse strong { font: 600 22px/1.1 var(--font-sans, inherit); letter-spacing: -.02em; font-variant-numeric: tabular-nums; } +.goal-team-pulse span { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--pw-muted, #666); } +.goal-team-pulse li[data-empty="true"] strong { color: var(--pw-faint, #a1a1a1); } +.goal-team-pulse li[data-empty="false"][data-bucket="attention"] strong, .goal-team-pulse li[data-empty="false"][data-bucket="attention"] span { color: var(--pw-red, #b42318); } +.goal-team-pulse li[data-empty="false"][data-bucket="accepted"] span { color: var(--pw-green, #13804a); } +.goal-team-pulse li[data-empty="false"][data-bucket="executing"] span, .goal-team-pulse li[data-empty="false"][data-bucket="validating"] span { color: var(--pw-blue-ink, #0060d0); } +.goal-team-work .goal-team-bindings { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; } +.goal-team-bindings > li { display: flex; flex-direction: column; gap: 8px; min-width: 0; padding: 14px 16px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 12px; background: var(--pw-surface, #fff); overflow-wrap: anywhere; } +.goal-team-bindings > li[data-check="blocked"] { border-color: color-mix(in srgb, var(--pw-red, #b42318) 30%, var(--pw-line, #ebebeb)); } +.goal-team-bindings > li > p { margin: 0; font-size: 12px; line-height: 1.55; } +.goal-team-bindings > li > p[role="alert"] { color: var(--pw-red, #b42318); } +.goal-team-member-head { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 4px 10px; } +.goal-team-member-head > strong { font-size: 14px; font-weight: 600; overflow-wrap: anywhere; } +.goal-team-avatar { grid-row: span 2; display: grid; place-items: center; width: 34px; height: 34px; border-radius: 9999px; background: var(--pw-text, #171717); color: var(--pw-surface, #fff); font: 600 13px/1 var(--font-mono, monospace); } +.goal-team-member-head .goal-team-icon-button { grid-row: span 2; grid-column: 3; min-width: 36px; min-height: 36px; padding: 0; } +.goal-team-check-pill { grid-column: 2; justify-self: start; display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: 9999px; border: 1px solid var(--pw-line, #ebebeb); font-size: 11.5px; color: var(--pw-muted, #666); } +li[data-check="ready"] .goal-team-check-pill { border-color: transparent; background: var(--pw-green-bg, #e6f3ec); color: var(--pw-green, #13804a); } +li[data-check="unverified"] .goal-team-check-pill { border-color: transparent; background: #fff6df; color: var(--pw-amber, #825a00); } +li[data-check="blocked"] .goal-team-check-pill { border-color: transparent; background: var(--pw-red-bg, #fcecea); color: var(--pw-red, #b42318); } +.goal-team-work .goal-team-records { display: grid; gap: 6px; margin: 0; } +.goal-team-record { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; padding: 6px 6px 6px 10px; border-radius: 6px; background: var(--pw-bg, #fafafa); } +.goal-team-record-state { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 500; } +.goal-team-record[data-state="accepted"] .goal-team-record-state { color: var(--pw-green, #13804a); } +.goal-team-record:is([data-state="rejected"], [data-state="recovery_required"], [data-state="unavailable"]) .goal-team-record-state { color: var(--pw-red, #b42318); } +.goal-team-record:is([data-state="executing"], [data-state="validating"]) .goal-team-record-state { color: var(--pw-blue-ink, #0060d0); } +.goal-loopx-mode .goal-team-record > button { min-height: 32px; padding: 4px 10px; font-size: 12px; } +.goal-team-records-empty { font-size: 12px; color: var(--pw-faint, #a1a1a1); } +.goal-team-live { width: 8px; height: 8px; border-radius: 50%; background: currentColor; box-shadow: 0 0 0 0 currentColor; animation: goal-team-live 1.6s ease-out infinite; } +@keyframes goal-team-live { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 45%, transparent); } 100% { box-shadow: 0 0 0 6px transparent; } } +@media (prefers-reduced-motion: reduce) { .goal-team-live { animation: none; } } +.goal-team-work .goal-team-warning { display: flex; align-items: flex-start; gap: 8px; padding: 10px 12px; border-radius: 9px; background: #fff6df; color: var(--pw-amber, #825a00); } +.goal-team-work .goal-team-warning > svg { flex: 0 0 auto; margin-top: 2px; } +.goal-loopx-dialog:has(.goal-team-bindings) { width: min(820px, calc(100vw - 32px)); } @media (max-width: 640px) { .goal-loopx-mode { padding: 6px 14px; } .goal-loopx-mode-bar { flex-wrap: wrap; gap: 0; } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-team-work.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-team-work.tsx index 79a6ef2334..2208207691 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-team-work.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-team-work.tsx @@ -1,10 +1,51 @@ import {useEffect, useRef, useState} from "react"; -import {fetchLoopXTeamWork, inspectLoopXMember, delegationStateLabel, type LoopXModeSnapshot, type DelegationInventory, type DelegationPreflight} from "../../data/chat"; +import {AlertTriangle, CheckCircle2, CircleDashed, CircleHelp, Clock3, Loader2, RotateCcw, ShieldCheck} from "lucide-react"; +import {fetchLoopXTeamWork, inspectLoopXMember, delegationState, delegationStateLabel, type DelegationState, type LoopXModeSnapshot, type DelegationInventory, type DelegationPreflight} from "../../data/chat"; import {GoalTeamEvidence} from "./goal-team-evidence"; import {DelegationPreflightStatus} from "./delegation-preflight-status"; type Member = {id: string; agent_id: string; todo_id: string}; +type DelegationRecord = DelegationInventory["items"][number]; +type PulseBucket = "executing" | "validating" | "accepted" | "attention" | "dispatched" | "unknown"; +type CheckTone = "unchecked" | "ready" | "unverified" | "blocked"; + +const PULSE_BUCKETS: Record = { + executing: "executing", validating: "validating", accepted: "accepted", + rejected: "attention", recovery_required: "attention", unavailable: "attention", + dispatched: "dispatched", unknown: "unknown", +}; +const PULSE_LABELS: Record = { + executing: {zh: "执行中", en: "Executing"}, + validating: {zh: "正在验收", en: "Validating"}, + accepted: {zh: "已通过", en: "Accepted"}, + attention: {zh: "需要处理", en: "Needs attention"}, + dispatched: {zh: "等待回读", en: "Awaiting readback"}, + unknown: {zh: "状态未知", en: "Unknown"}, +}; +const CHECK_LABELS: Record = { + unchecked: {zh: "未检查", en: "Not checked"}, + ready: {zh: "可启动", en: "Ready"}, + unverified: {zh: "运行时待核验", en: "Runtime unverified"}, + blocked: {zh: "受阻", en: "Blocked"}, +}; + +function checkTone(check: DelegationPreflight | undefined, failed: boolean): CheckTone { + if (failed) return "blocked"; + if (!check) return "unchecked"; + return check.state === "launchable" ? "ready" : check.state === "runtime_unverified" ? "unverified" : "blocked"; +} + +function StateIcon({state}: {state: DelegationState | PulseBucket | CheckTone}) { + if (state === "executing") return