Merge pull request #815 from pikasTech/fix/issue-803-system-intro

fix(web): v0.2 drop Agent workspace intro System cards, fold 界面模式 into panel title hint (HWLAB #803 followup)
This commit is contained in:
Lyon
2026-06-04 13:50:31 +08:00
committed by GitHub
6 changed files with 19 additions and 164 deletions
+12
View File
@@ -82,6 +82,9 @@ const data = await page.evaluate(() => {
const convListRect = convList?.getBoundingClientRect();
const messages = [...document.querySelectorAll(".message-card")].map((card) => {
const rect = card.getBoundingClientRect();
const role = card.getAttribute("data-message-role")
?? [...card.classList].find((cls) => cls.startsWith("message-"))?.replace(/^message-/, "")
?? null;
const meta = card.querySelector(".message-meta")?.textContent?.replace(/\s+/gu, " ").trim() ?? null;
const pending = card.querySelector(".message-pending-context")?.textContent?.replace(/\s+/gu, " ").trim().slice(0, 600) ?? null;
const session = card.querySelector(".message-session-context")?.textContent?.replace(/\s+/gu, " ").trim().slice(0, 600) ?? null;
@@ -92,6 +95,7 @@ const data = await page.evaluate(() => {
const cardWidth = rect.width;
const tracePct = cardWidth > 0 && traceEventsRect ? Math.round((traceEventsRect.width / cardWidth) * 100) : null;
return {
role,
meta,
pending,
session,
@@ -103,6 +107,12 @@ const data = await page.evaluate(() => {
traceEventsPct: tracePct
};
});
const systemCount = messages.filter((m) => m.role === "system").length;
const roleCounts = messages.reduce((acc, m) => {
const key = m.role ?? "unknown";
acc[key] = (acc[key] ?? 0) + 1;
return acc;
}, {});
return {
summaryText,
summaryDetailText,
@@ -112,6 +122,8 @@ const data = await page.evaluate(() => {
summaryDetailRect: summaryDetailRect ? { width: Math.round(summaryDetailRect.width), height: Math.round(summaryDetailRect.height) } : null,
convListRect: convListRect ? { width: Math.round(convListRect.width), height: Math.round(convListRect.height) } : null,
messageCount: messages.length,
systemMessageCount: systemCount,
roleCounts,
messages
};
});
@@ -23,25 +23,6 @@ interface ConversationPanelProps {
}
export function ConversationPanel({ messages, availability, live, chatPending, codeAgentTimeoutMs, onCopySession, onCancelMessage, onRetryMessage, onReplayTrace }: ConversationPanelProps & { codeAgentTimeoutMs: number }): ReactElement {
const intro = useMemo<ChatMessage[]>(() => [
{
id: "intro-mode",
role: "system",
title: "界面模式",
text: "这里是用户工作台:可以整理任务、查看 Device Pod summary 和纯文本事件流。对话会发送到受控 Code Agent 后端;页面不会直接发送硬件变更。",
status: "source",
createdAt: new Date(0).toISOString()
},
{
id: "intro-code-agent",
role: "system",
title: "Code Agent 状态",
text: availabilitySummary(availability),
status: availability?.ready === true ? "completed" : "source",
createdAt: new Date(0).toISOString()
}
], [availability]);
return (
<section className="view workbench-view" id="workspace" data-view="workspace" aria-labelledby="workspace-title">
<div className="conversation-column">
@@ -50,6 +31,7 @@ export function ConversationPanel({ messages, availability, live, chatPending, c
<div>
<p className="eyebrow">Agent </p>
<h2 id="workspace-title">Agent </h2>
<p className="conversation-panel-hint" id="workspace-hint" data-workspace-hint> Device Pod summary Code Agent </p>
</div>
<div className="panel-title-actions">
<button className="icon-button" id="copy-session-id" type="button" aria-label="复制 session id" title="复制 session id 到剪贴板,便于在 HWLAB CLI 复现。" onClick={onCopySession}> session id</button>
@@ -58,7 +40,10 @@ export function ConversationPanel({ messages, availability, live, chatPending, c
</div>
<CodeAgentSummary availability={availability} live={live} latestMessage={latestAgentMessage(messages)} />
<div className="conversation-list" id="conversation-list" aria-live="polite">
{[...intro, ...messages].map((message) => (
{messages.length === 0 ? (
<p className="conversation-empty" id="conversation-empty" data-conversation-empty> Command Bar Agent </p>
) : null}
{messages.map((message) => (
<MessageCard
key={message.id}
message={message}
@@ -238,8 +223,3 @@ function statusLabel(message: ChatMessage): { label: string; tone: ReturnType<ty
const label = agentStatusLabel(message);
return { label: label.label, tone: label.tone };
}
function availabilitySummary(availability: CodeAgentAvailability | null): string {
if (!availability) return "正在读取 live 后端和 Code Agent 可用性。";
return `Code Agent ${availability.status ?? "unknown"}provider=${availability.provider ?? "unknown"}backend=${availability.backend ?? "unknown"}`;
}
@@ -1,43 +0,0 @@
import type { ReactElement } from "react";
import type { ChatMessage } from "../../types/domain";
import { formatBeijingTime, formatDuration, shortToken, toneClass } from "../../utils";
interface MessagePendingContextProps {
message: ChatMessage;
codeAgentTimeoutMs: number;
}
export function MessagePendingContext({ message, codeAgentTimeoutMs }: MessagePendingContextProps): ReactElement | null {
if (message.status !== "running") return null;
const fields: Array<[string, string | null | undefined, boolean]> = [
["状态", "处理中", false],
["traceId", typeof message.traceId === "string" ? message.traceId : null, true],
["conversation", typeof message.conversationId === "string" ? message.conversationId : null, true],
["session", message.sessionId ?? message.runnerTrace?.sessionId ?? "等待后端分配", true],
["thread", message.threadId ?? message.runnerTrace?.threadId ?? "等待后端分配", false],
["sessionStatus", typeof message.runnerTrace?.sessionStatus === "string" ? message.runnerTrace.sessionStatus : null, false],
["lifecycle", typeof message.runnerTrace?.sessionLifecycleStatus === "string" ? message.runnerTrace.sessionLifecycleStatus : null, false],
["activityTimeout", `${formatDuration(codeAgentTimeoutMs)} 无新事件`, false]
];
return (
<section className="message-pending-context tone-border-pending" data-message-pending-context>
<div className="message-pending-head">
<span className={`state-tag tone-${toneClass("pending")}`} data-pending-tag></span>
<span className="message-pending-summary">{message.runnerTrace?.waitingFor ? `等待 ${message.runnerTrace.waitingFor}` : "等待后端事件"}</span>
</div>
<div className="message-pending-grid" role="list">
{fields.map(([label, value, copyable]) => {
if (value === undefined || value === null || value === "") return null;
return (
<div key={label} className="message-pending-row" role="listitem">
<span className="message-pending-key">{label}</span>
<span className="message-pending-value mono">{copyable ? shortToken(value, 32) : value}</span>
<span className="message-pending-time">{formatBeijingTime(message.updatedAt ?? message.createdAt, { withSeconds: true })}</span>
</div>
);
})}
</div>
</section>
);
}
@@ -1,37 +0,0 @@
import type { ReactElement } from "react";
import type { ChatMessage } from "../../types/domain";
import { codeAgentRuntimePathFromMessage } from "../../state/code-agent-status";
import { toneClass } from "../../utils";
interface MessageRuntimePathProps {
message: ChatMessage;
}
export function MessageRuntimePath({ message }: MessageRuntimePathProps): ReactElement | null {
if (message.role !== "agent") return null;
const runtimePath = codeAgentRuntimePathFromMessage(message as unknown as Record<string, unknown>);
if (!runtimePath) return null;
return (
<details className={`message-runtime-path tone-border-${toneClass(runtimePath.tone)}`} data-message-runtime-path>
<summary>
<span className={`state-tag tone-${toneClass(runtimePath.tone)}`} data-runtime-path-tag>{runtimePath.label}</span>
<span className="message-runtime-summary">{runtimePath.summary}</span>
</summary>
<div className="message-runtime-grid" role="list">
{runtimePath.rows.map((row) => (
<div key={row.label} className={`message-runtime-row${row.value === "未观测" || row.value === "字段缺失:证据不足" ? " message-runtime-row-missing" : ""}`} role="listitem">
<span className="message-runtime-key">{row.label}</span>
<span className="message-runtime-value mono">{row.value}</span>
</div>
))}
{runtimePath.missingFields.length > 0 ? (
<div className="message-runtime-row message-runtime-row-missing message-runtime-row-wide" role="listitem">
<span className="message-runtime-key"></span>
<span className="message-runtime-value mono">{runtimePath.missingFields.join(", ")}</span>
</div>
) : null}
</div>
</details>
);
}
@@ -1,59 +0,0 @@
import type { ReactElement } from "react";
import type { ChatMessage } from "../../types/domain";
import { nonEmptyString } from "../../utils";
import { formatBeijingTime, shortToken, toneClass } from "../../utils";
interface MessageSessionContinuityProps {
message: ChatMessage;
}
interface ContinuityField {
label: string;
value: string | null;
copyable: boolean;
}
export function MessageSessionContinuity({ message }: MessageSessionContinuityProps): ReactElement | null {
if (message.role !== "agent" || message.status === "running") return null;
const sessionLifecycle = pickString(message.runnerTrace?.sessionLifecycleStatus);
const hint = pickString(message.runnerTrace?.sessionLifecycleHint);
const lastEventLabel = pickString(message.runnerTrace?.lastEventLabel);
const fields: ContinuityField[] = [
{ label: "状态", value: statusLabel(message.status), copyable: false },
{ label: "conversation", value: message.conversationId ?? "未确认", copyable: true },
{ label: "session", value: message.sessionId ?? "未确认", copyable: true },
{ label: "thread", value: message.threadId ?? "未确认", copyable: true },
{ label: "lifecycle", value: sessionLifecycle, copyable: false },
{ label: "提示", value: hint, copyable: false },
{ label: "traceId", value: message.traceId ?? "未确认", copyable: true },
{ label: "lastEvent", value: lastEventLabel, copyable: false }
].filter((field): field is ContinuityField => Boolean(field.value && field.value !== "未确认"));
if (fields.length === 0) return null;
return (
<details className={`message-session-context tone-border-${toneClass(message.status)}`} data-message-session-continuity>
<summary>
<span className={`state-tag tone-${toneClass(message.status)}`} data-session-continuity-tag> {statusLabel(message.status)}</span>
<span className="message-session-summary">{hint ?? lastEventLabel ?? "会话上下文已记录"}</span>
</summary>
<div className="message-session-grid" role="list">
{fields.map((field) => (
<div key={field.label} className="message-session-row" role="listitem">
<span className="message-session-key">{field.label}</span>
<span className="message-session-value mono">{field.copyable ? shortToken(field.value, 32) : field.value}</span>
<span className="message-session-time">{formatBeijingTime(message.updatedAt ?? message.createdAt, { withSeconds: true })}</span>
</div>
))}
</div>
</details>
);
}
function statusLabel(status: string): string {
const map: Record<string, string> = { completed: "完成", failed: "失败", blocked: "阻塞", timeout: "超时", canceled: "已取消", source: "SOURCE" };
return map[status] ?? status ?? "未知";
}
function pickString(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
@@ -53,6 +53,8 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
.session-sidebar { min-width: 0; display: grid; grid-template-rows: auto auto minmax(0, 1fr) auto; gap: 10px; padding: 14px; border-right: 1px solid var(--line); background: #f9faf6; overflow: hidden; }
.session-sidebar-head, .panel-title-row, .right-sidebar-head, .gate-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.session-sidebar h2, .panel-title-row h2, .right-sidebar h2 { margin: 0; font-size: 18px; }
.conversation-panel-hint { margin: 4px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; max-width: 60ch; }
.conversation-empty { margin: 12px 0; padding: 16px; border: 1px dashed var(--line); border-radius: 6px; color: var(--muted); font-size: 13px; text-align: center; }
.session-icon-button, .icon-button, .right-sidebar-toggle, .session-delete-button, .command-button.secondary, #device-event-follow, #device-event-jump { border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); padding: 8px 10px; font-weight: 700; }
.session-tabs { min-height: 0; display: grid; align-content: start; gap: 8px; overflow-x: hidden; overflow-y: auto; }
.session-tab { display: grid; gap: 3px; text-align: left; border: 1px solid var(--line); border-radius: 6px; padding: 9px; background: var(--surface); }