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:
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user