Merge pull request #822 from pikasTech/fix/issue-816-ui-density
fix: compact v0.2 code agent workbench layout
This commit is contained in:
@@ -8,6 +8,7 @@ import { useWorkbenchStore } from "./state/workbench";
|
||||
import type { RouteId } from "./types/domain";
|
||||
import { LoginShell } from "./components/auth/LoginShell";
|
||||
import { CommandBar } from "./components/command-bar/CommandBar";
|
||||
import { recordDraft } from "./components/command-bar/DraftsList";
|
||||
import { ConversationPanel } from "./components/conversation/ConversationPanel";
|
||||
import { DevicePodSidebar } from "./components/device-pod/DevicePodSidebar";
|
||||
import { ActivityRail } from "./components/layout/ActivityRail";
|
||||
@@ -26,6 +27,14 @@ const RIGHT_SIDEBAR_DEFAULT_WIDTH = 728;
|
||||
const RIGHT_SIDEBAR_MIN_WIDTH = 560;
|
||||
const RIGHT_SIDEBAR_MAX_WIDTH = 900;
|
||||
|
||||
const routeLabels: Record<RouteId, string> = {
|
||||
workspace: "工作台",
|
||||
skills: "Skills",
|
||||
gate: "内部复核",
|
||||
help: "使用说明",
|
||||
settings: "设置"
|
||||
};
|
||||
|
||||
export function App(): ReactElement {
|
||||
const auth = useAuth();
|
||||
const store = useWorkbenchStore(auth.authState === "authenticated");
|
||||
@@ -92,6 +101,9 @@ export function App(): ReactElement {
|
||||
}, [route]);
|
||||
|
||||
const modelLabel = useMemo(() => store.state.providerProfile === "codex-api" ? "Codex API" : store.state.providerProfile === "minimax-m3" ? "MiniMax-M3" : "DeepSeek", [store.state.providerProfile]);
|
||||
const workspaceRoute = route === "workspace";
|
||||
const sessionSidebarVisible = workspaceRoute && !leftCollapsed;
|
||||
const devicePodSidebarVisible = workspaceRoute && !rightCollapsed;
|
||||
|
||||
function navigate(nextRoute: RouteId): void {
|
||||
window.location.hash = `/${nextRoute}`;
|
||||
@@ -109,64 +121,83 @@ export function App(): ReactElement {
|
||||
data-help-route-policy="non-default-internal-help"
|
||||
data-internal-help-path="/help.md"
|
||||
>
|
||||
<ActivityRail route={route} collapsed={leftCollapsed} onRoute={navigate} onToggle={() => setLeftCollapsed((value) => !value)} />
|
||||
<SessionSidebar tabs={store.sessionTabs} loading={store.state.loading} modelLabel={modelLabel} onCreate={() => void store.createSession()} onDelete={() => void store.deleteCurrentSession()} onSelect={(tab) => void store.selectConversation(tab)} />
|
||||
<div
|
||||
className="resize-handle session-sidebar-resize"
|
||||
id="session-sidebar-resize"
|
||||
role="separator"
|
||||
tabIndex={narrow || leftCollapsed ? -1 : 0}
|
||||
aria-disabled={narrow || leftCollapsed}
|
||||
aria-hidden={narrow || leftCollapsed}
|
||||
aria-label="拖拽调整 Session sidebar 宽度"
|
||||
aria-controls="session-sidebar"
|
||||
aria-orientation="vertical"
|
||||
aria-valuemin={sessionResize.bounds.min}
|
||||
aria-valuemax={sessionResize.bounds.max}
|
||||
aria-valuenow={sessionResize.ariaValueNow}
|
||||
aria-valuetext={sessionResize.ariaValueText}
|
||||
title="拖拽调整 Session sidebar 宽度;用左右方向键微调,Home/End 到最小或最大。"
|
||||
onPointerDown={sessionResize.onPointerDown}
|
||||
onPointerMove={sessionResize.onPointerMove}
|
||||
onPointerUp={sessionResize.onPointerUp}
|
||||
onPointerCancel={sessionResize.onPointerCancel}
|
||||
onKeyDown={sessionResize.onKeyDown}
|
||||
/>
|
||||
<section className="center-workspace">
|
||||
{route === "workspace" ? <ConversationPanel messages={store.state.messages} availability={store.state.codeAgentAvailability} live={store.state.live} chatPending={store.state.chatPending} codeAgentTimeoutMs={store.state.codeAgentTimeoutMs} onCopySession={() => void navigator.clipboard?.writeText(store.activeConversationId ?? "")} onCancelMessage={(id) => void store.cancelAgentMessage(id)} onRetryMessage={(id) => void store.retryAgentMessage(id)} onReplayTrace={(id) => void store.replayAgentTrace(id)} /> : null}
|
||||
{route === "skills" ? <SkillsView /> : null}
|
||||
{route === "gate" ? <GateView /> : null}
|
||||
{route === "help" ? <HelpView help={help} /> : null}
|
||||
{route === "settings" ? <SettingsView onLogout={auth.logout} /> : null}
|
||||
<DraftsList onPick={pickDraft} />
|
||||
<CommandBar key={store.activeConversationId ?? "default"} disabled={store.composer.disabled} submitMode={store.composer.submitMode} targetTraceId={store.composer.targetTraceId} providerProfile={store.state.providerProfile} codeAgentTimeoutMs={store.state.codeAgentTimeoutMs} gatewayShellTimeoutMs={store.state.gatewayShellTimeoutMs} onProviderProfile={store.setProviderProfile} onCodeAgentTimeout={store.setCodeAgentTimeoutMs} onGatewayShellTimeout={store.setGatewayShellTimeoutMs} onSubmit={submitWithDraftRecord} onClear={store.clearConversation} pickedDraft={pickedDraft} onPickedDraftConsumed={clearPickedDraft} disabledReason={store.composer.disabledReason} onTyping={noteActivity} />
|
||||
<ActivityRail route={route} onRoute={navigate} />
|
||||
<section className="main-workspace" id="main-workspace" aria-label="主工作区">
|
||||
<header className="workspace-topbar" id="workspace-topbar" aria-label="主工作区状态栏">
|
||||
<button className="workspace-topbar-button" id="left-sidebar-toggle" type="button" aria-controls="session-sidebar" aria-expanded={sessionSidebarVisible} aria-label={leftCollapsed ? "展开左侧 Session sidebar" : "折叠左侧 Session sidebar"} title={leftCollapsed ? "展开左侧 Session sidebar" : "折叠左侧 Session sidebar"} disabled={!workspaceRoute} onClick={() => setLeftCollapsed((value) => !value)}>{leftCollapsed ? "Sessions" : "隐藏 Sessions"}</button>
|
||||
<div className="workspace-topbar-status">
|
||||
<span>{routeLabels[route]}</span>
|
||||
<StatePill tone={store.state.chatPending ? "pending" : "source"}>{store.state.chatPending ? "处理中" : modelLabel}</StatePill>
|
||||
</div>
|
||||
<button className="workspace-topbar-button" id="right-sidebar-toggle" type="button" aria-controls="device-pod-sidebar" aria-expanded={devicePodSidebarVisible} aria-label={rightCollapsed ? "展开右侧 Device Pod 看板" : "折叠右侧 Device Pod 看板"} title={rightCollapsed ? "展开右侧 Device Pod 看板" : "折叠右侧 Device Pod 看板"} disabled={!workspaceRoute} onClick={() => setRightCollapsed((value) => !value)}>{rightCollapsed ? "Device Pod" : "隐藏 Device Pod"}</button>
|
||||
</header>
|
||||
<div className={`workspace-body${!workspaceRoute ? " is-route-without-sidebars" : ""}`}>
|
||||
{workspaceRoute ? <SessionSidebar tabs={store.sessionTabs} loading={store.state.loading} modelLabel={modelLabel} onCreate={() => void store.createSession()} onDelete={() => void store.deleteCurrentSession()} onSelect={(tab) => void store.selectConversation(tab)} /> : null}
|
||||
{workspaceRoute ? (
|
||||
<div
|
||||
className="resize-handle session-sidebar-resize"
|
||||
id="session-sidebar-resize"
|
||||
role="separator"
|
||||
tabIndex={narrow || leftCollapsed ? -1 : 0}
|
||||
aria-disabled={narrow || leftCollapsed}
|
||||
aria-hidden={narrow || leftCollapsed}
|
||||
aria-label="拖拽调整 Session sidebar 宽度"
|
||||
aria-controls="session-sidebar"
|
||||
aria-orientation="vertical"
|
||||
aria-valuemin={sessionResize.bounds.min}
|
||||
aria-valuemax={sessionResize.bounds.max}
|
||||
aria-valuenow={sessionResize.ariaValueNow}
|
||||
aria-valuetext={sessionResize.ariaValueText}
|
||||
title="拖拽调整 Session sidebar 宽度;用左右方向键微调,Home/End 到最小或最大。"
|
||||
onPointerDown={sessionResize.onPointerDown}
|
||||
onPointerMove={sessionResize.onPointerMove}
|
||||
onPointerUp={sessionResize.onPointerUp}
|
||||
onPointerCancel={sessionResize.onPointerCancel}
|
||||
onKeyDown={sessionResize.onKeyDown}
|
||||
/>
|
||||
) : null}
|
||||
<section className="center-workspace">
|
||||
{route === "workspace" ? <ConversationPanel messages={store.state.messages} availability={store.state.codeAgentAvailability} live={store.state.live} chatPending={store.state.chatPending} codeAgentTimeoutMs={store.state.codeAgentTimeoutMs} onCopySession={() => void navigator.clipboard?.writeText(store.activeConversationId ?? "")} onCancelMessage={(id) => void store.cancelAgentMessage(id)} onRetryMessage={(id) => void store.retryAgentMessage(id)} onReplayTrace={(id) => void store.replayAgentTrace(id)} /> : null}
|
||||
{route === "skills" ? <SkillsView /> : null}
|
||||
{route === "gate" ? <GateView /> : null}
|
||||
{route === "help" ? <HelpView help={help} /> : null}
|
||||
{route === "settings" ? <SettingsView providerProfile={store.state.providerProfile} codeAgentTimeoutMs={store.state.codeAgentTimeoutMs} gatewayShellTimeoutMs={store.state.gatewayShellTimeoutMs} onProviderProfile={store.setProviderProfile} onCodeAgentTimeout={store.setCodeAgentTimeoutMs} onGatewayShellTimeout={store.setGatewayShellTimeoutMs} onLogout={auth.logout} /> : null}
|
||||
{workspaceRoute ? <CommandBar key={store.activeConversationId ?? "default"} disabled={store.composer.disabled} submitMode={store.composer.submitMode} targetTraceId={store.composer.targetTraceId} providerProfile={store.state.providerProfile} onProviderProfile={store.setProviderProfile} onSubmit={submitWithDraftRecord} onClear={store.clearConversation} pickedDraft={pickedDraft} onPickedDraftConsumed={clearPickedDraft} disabledReason={store.composer.disabledReason} onTyping={noteActivity} onPickDraft={pickDraft} /> : null}
|
||||
</section>
|
||||
{workspaceRoute ? (
|
||||
<div
|
||||
className="resize-handle right-sidebar-resize"
|
||||
id="right-sidebar-resize"
|
||||
role="separator"
|
||||
tabIndex={narrow || rightCollapsed ? -1 : 0}
|
||||
aria-disabled={narrow || rightCollapsed}
|
||||
aria-hidden={narrow || rightCollapsed}
|
||||
aria-label="拖拽调整右侧 Device Pod 看板宽度"
|
||||
aria-controls="device-pod-sidebar"
|
||||
aria-orientation="vertical"
|
||||
aria-valuemin={rightResize.bounds.min}
|
||||
aria-valuemax={rightResize.bounds.max}
|
||||
aria-valuenow={rightResize.ariaValueNow}
|
||||
aria-valuetext={rightResize.ariaValueText}
|
||||
title="拖拽调整右侧 Device Pod 看板宽度;用左右方向键微调,Home/End 到最小或最大。"
|
||||
onPointerDown={rightResize.onPointerDown}
|
||||
onPointerMove={rightResize.onPointerMove}
|
||||
onPointerUp={rightResize.onPointerUp}
|
||||
onPointerCancel={rightResize.onPointerCancel}
|
||||
onKeyDown={rightResize.onKeyDown}
|
||||
/>
|
||||
) : null}
|
||||
{workspaceRoute ? <DevicePodSidebar live={store.state.live} selectedDevicePodId={store.state.selectedDevicePodId} onSelect={store.selectDevicePod} collapsed={rightCollapsed} /> : null}
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
className="resize-handle right-sidebar-resize"
|
||||
id="right-sidebar-resize"
|
||||
role="separator"
|
||||
tabIndex={narrow || rightCollapsed ? -1 : 0}
|
||||
aria-disabled={narrow || rightCollapsed}
|
||||
aria-hidden={narrow || rightCollapsed}
|
||||
aria-label="拖拽调整右侧 Device Pod 看板宽度"
|
||||
aria-controls="device-pod-sidebar"
|
||||
aria-orientation="vertical"
|
||||
aria-valuemin={rightResize.bounds.min}
|
||||
aria-valuemax={rightResize.bounds.max}
|
||||
aria-valuenow={rightResize.ariaValueNow}
|
||||
aria-valuetext={rightResize.ariaValueText}
|
||||
title="拖拽调整右侧 Device Pod 看板宽度;用左右方向键微调,Home/End 到最小或最大。"
|
||||
onPointerDown={rightResize.onPointerDown}
|
||||
onPointerMove={rightResize.onPointerMove}
|
||||
onPointerUp={rightResize.onPointerUp}
|
||||
onPointerCancel={rightResize.onPointerCancel}
|
||||
onKeyDown={rightResize.onKeyDown}
|
||||
/>
|
||||
<DevicePodSidebar live={store.state.live} selectedDevicePodId={store.state.selectedDevicePodId} onSelect={store.selectDevicePod} collapsed={rightCollapsed} onToggle={() => setRightCollapsed((value) => !value)} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function StatePill({ tone, children }: { tone: "pending" | "source"; children: string }): ReactElement {
|
||||
return <span className={`workspace-topbar-pill tone-${tone}`}>{children}</span>;
|
||||
}
|
||||
|
||||
function useMatchMedia(query: string): boolean {
|
||||
const [match, setMatch] = useState<boolean>(() => typeof window !== "undefined" && window.matchMedia(query).matches);
|
||||
useEffect(() => {
|
||||
@@ -209,4 +240,3 @@ function routeFromLocation(): RouteId {
|
||||
if (path === "/gate" || path === "/diagnostics/gate") return "gate";
|
||||
return "workspace";
|
||||
}
|
||||
import { DraftsList, recordDraft } from "./components/command-bar/DraftsList";
|
||||
|
||||
@@ -2,17 +2,14 @@ import type { ReactElement } from "react";
|
||||
import { FormEvent, KeyboardEvent, useEffect, useState } from "react";
|
||||
|
||||
import type { ProviderProfile } from "../../types/domain";
|
||||
import { DraftsList } from "./DraftsList";
|
||||
|
||||
interface CommandBarProps {
|
||||
disabled: boolean;
|
||||
submitMode: "turn" | "steer";
|
||||
targetTraceId: string | null;
|
||||
providerProfile: ProviderProfile;
|
||||
codeAgentTimeoutMs: number;
|
||||
gatewayShellTimeoutMs: number;
|
||||
onProviderProfile(profile: ProviderProfile): void;
|
||||
onCodeAgentTimeout(value: number): void;
|
||||
onGatewayShellTimeout(value: number): void;
|
||||
onSubmit(value: string): Promise<void>;
|
||||
onClear(): void;
|
||||
pickedDraft: string | null;
|
||||
@@ -27,11 +24,13 @@ interface CommandBarProps {
|
||||
* backend has not yet emitted the next trace event.
|
||||
*/
|
||||
onTyping(): void;
|
||||
onPickDraft(draft: string): void;
|
||||
}
|
||||
|
||||
export function CommandBar({ pickedDraft, onPickedDraftConsumed, disabledReason, onTyping, ...props }: CommandBarProps): ReactElement {
|
||||
const [value, setValue] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [draftsOpen, setDraftsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (pickedDraft) {
|
||||
@@ -67,34 +66,14 @@ export function CommandBar({ pickedDraft, onPickedDraftConsumed, disabledReason,
|
||||
<option value="minimax-m3">MiniMax-M3</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="agent-timeout-control" htmlFor="code-agent-timeout">
|
||||
<span>无新事件超时</span>
|
||||
<select id="code-agent-timeout" aria-label="Code Agent 无新事件超时" value={props.codeAgentTimeoutMs} onChange={(event) => props.onCodeAgentTimeout(Number(event.currentTarget.value))}>
|
||||
<option value="180000">3 分钟</option>
|
||||
<option value="300000">5 分钟</option>
|
||||
<option value="600000">10 分钟</option>
|
||||
<option value="900000">15 分钟</option>
|
||||
<option value="1200000">20 分钟</option>
|
||||
<option value="1800000">30 分钟</option>
|
||||
<option value="2400000">40 分钟</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="agent-timeout-control" htmlFor="gateway-shell-timeout">
|
||||
<span>Gateway 命令超时</span>
|
||||
<select id="gateway-shell-timeout" aria-label="PC gateway shell 命令超时" value={props.gatewayShellTimeoutMs} onChange={(event) => props.onGatewayShellTimeout(Number(event.currentTarget.value))}>
|
||||
<option value="60000">1 分钟</option>
|
||||
<option value="120000">2 分钟</option>
|
||||
<option value="180000">3 分钟</option>
|
||||
<option value="300000">5 分钟</option>
|
||||
<option value="600000">10 分钟</option>
|
||||
</select>
|
||||
</label>
|
||||
<div className="input-shell">
|
||||
<span className="prompt-mark">hwlab</span>
|
||||
<textarea id="command-input" value={value} placeholder="输入任务,Enter 发送,Shift+Enter 换行" rows={1} onChange={(event) => { setValue(event.currentTarget.value); onTyping(); }} onKeyDown={keyDown} />
|
||||
</div>
|
||||
<button className="command-button secondary command-drafts-toggle" id="command-drafts-toggle" type="button" aria-expanded={draftsOpen} aria-controls="command-drafts-panel" onClick={() => setDraftsOpen((open) => !open)}>最近输入</button>
|
||||
<button className="command-button" id="command-send" type="submit" disabled={props.disabled || submitting}>{sendLabel(props.submitMode, submitting)}</button>
|
||||
<button className="command-button secondary" id="command-clear" type="button" onClick={() => { setValue(""); props.onClear(); }}>清空</button>
|
||||
{draftsOpen ? <DraftsList id="command-drafts-panel" onPick={(draft) => { props.onPickDraft(draft); setDraftsOpen(false); }} /> : null}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { shortToken } from "../../utils";
|
||||
|
||||
interface DraftsListProps {
|
||||
onPick(draft: string): void;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
interface DraftEntry {
|
||||
@@ -15,7 +16,7 @@ interface DraftEntry {
|
||||
const DRAFTS_KEY = "hwlab.workbench.recentDrafts.v1";
|
||||
const DRAFTS_LIMIT = 8;
|
||||
|
||||
export function DraftsList({ onPick }: DraftsListProps): ReactElement | null {
|
||||
export function DraftsList({ onPick, id }: DraftsListProps): ReactElement | null {
|
||||
const [drafts, setDrafts] = useState<DraftEntry[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -39,7 +40,7 @@ export function DraftsList({ onPick }: DraftsListProps): ReactElement | null {
|
||||
|
||||
if (drafts.length === 0) return null;
|
||||
return (
|
||||
<section className="drafts-list" data-drafts-list aria-label="最近发送过的草稿">
|
||||
<section className="drafts-list" id={id} data-drafts-list aria-label="最近发送过的草稿">
|
||||
<header className="drafts-list-head">
|
||||
<p className="eyebrow">最近输入</p>
|
||||
<button type="button" className="command-button secondary" onClick={() => { try { window.localStorage.removeItem(DRAFTS_KEY); } catch { /* ignore */ } setDrafts([]); }}>清空</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactElement } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { renderMessageMarkdown } from "../../services/markdown/render";
|
||||
import type { ChatMessage, CodeAgentAvailability, LiveSurface } from "../../types/domain";
|
||||
@@ -36,9 +36,9 @@ export function ConversationPanel({ messages, availability, live, chatPending, c
|
||||
<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>
|
||||
<StateTag id="agent-chat-status" tone={chatPending ? "pending" : "source"}>{chatPending ? "处理中" : "等待输入"}</StateTag>
|
||||
<CodeAgentSummary availability={availability} live={live} latestMessage={latestAgentMessage(messages)} />
|
||||
</div>
|
||||
</div>
|
||||
<CodeAgentSummary availability={availability} live={live} latestMessage={latestAgentMessage(messages)} />
|
||||
<div className="conversation-list" id="conversation-list" aria-live="polite">
|
||||
{messages.length === 0 ? (
|
||||
<p className="conversation-empty" id="conversation-empty" data-conversation-empty>暂无对话。在下方 Command Bar 输入任务,Agent 回复会出现在这里。</p>
|
||||
@@ -71,7 +71,6 @@ function latestAgentMessage(messages: ChatMessage[]): Record<string, unknown> |
|
||||
function CodeAgentSummary({ availability, live, latestMessage }: { availability: CodeAgentAvailability | null; live: LiveSurface | null; latestMessage: Record<string, unknown> | null }): ReactElement {
|
||||
const summary = useMemo(() => classifyCodeAgentStatusSummary({ availability, live, latestMessage }), [availability, live, latestMessage]);
|
||||
const rows = useMemo<DebugRow[]>(() => codeAgentSummaryRows(summary).map((row) => ({ label: row.key, value: row.value, tone: row.tone as DebugRow["tone"] })), [summary]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const sections: DebugSection[] = [
|
||||
{ title: "Code Agent 详情", rows },
|
||||
{ title: "readiness blockers", rows: summary.readinessBlockers.length > 0
|
||||
@@ -79,30 +78,10 @@ function CodeAgentSummary({ availability, live, latestMessage }: { availability:
|
||||
: [] , empty: summary.readinessBlockers.length === 0 ? "无" : undefined }
|
||||
];
|
||||
return (
|
||||
<div className="code-agent-summary-wrapper">
|
||||
<details
|
||||
className={`code-agent-summary tone-border-${toneClass(summary.tone)}`}
|
||||
id="code-agent-summary"
|
||||
data-code-agent-status-kind={summary.kind}
|
||||
open={open}
|
||||
onToggle={(event) => setOpen((event.currentTarget as HTMLDetailsElement).open)}
|
||||
>
|
||||
<summary>
|
||||
<span className={`code-agent-summary-icon tone-${toneClass(summary.tone)}`} id="code-agent-summary-icon" aria-hidden="true">{summary.icon}</span>
|
||||
<strong className={`code-agent-summary-label tone-${toneClass(summary.tone)}`} id="code-agent-summary-label">{summary.label}</strong>
|
||||
<span className="code-agent-summary-capability" id="code-agent-summary-capability">{summary.capabilityLevel}</span>
|
||||
</summary>
|
||||
<div className="code-agent-summary-detail" id="code-agent-summary-detail">
|
||||
<div className="code-agent-summary-rows" role="table" aria-label="Code Agent 状态详情">
|
||||
{rows.map((row, index) => (
|
||||
<div className="code-agent-summary-row" role="row" key={`${row.label}-${index}`}>
|
||||
<span className="code-agent-summary-key" role="rowheader">{row.label}</span>
|
||||
<span className={`code-agent-summary-value tone-${toneClass(row.tone)}`} role="cell">{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div className="code-agent-debug-summary" data-code-agent-status-kind={summary.kind}>
|
||||
<span className={`code-agent-summary-icon tone-${toneClass(summary.tone)}`} id="code-agent-summary-icon" aria-hidden="true">{summary.icon}</span>
|
||||
<span className={`code-agent-summary-label tone-${toneClass(summary.tone)}`} id="code-agent-summary-label">{summary.label}</span>
|
||||
<span className="code-agent-summary-capability" id="code-agent-summary-capability">{summary.capabilityLevel}</span>
|
||||
<DebugDialog
|
||||
dialogId="code-agent-debug"
|
||||
title="Code Agent 调试信息"
|
||||
@@ -119,15 +98,26 @@ function CodeAgentSummary({ availability, live, latestMessage }: { availability:
|
||||
function MessageCard({ message, codeAgentTimeoutMs, onCancel, onRetry, onReplayTrace }: { message: ChatMessage; codeAgentTimeoutMs: number; onCancel(): void; onRetry(): void; onReplayTrace(): void }): ReactElement {
|
||||
const html = renderMessageMarkdown(message.text);
|
||||
const traceKey = `hwlab.workbench.trace-open.${message.traceId ?? message.id}`;
|
||||
if (message.role === "agent") {
|
||||
const hasFinalResponse = message.status !== "running" && message.text.trim().length > 0;
|
||||
return (
|
||||
<article className={`message-card message-${message.role} tone-border-${toneClass(message.status)}`} data-message-status={message.status}>
|
||||
{message.runnerTrace ? <MessageTracePanel trace={message.runnerTrace} defaultOpen={message.status === "running"} storageKey={traceKey} /> : null}
|
||||
<section className="message-final-response" aria-label="Final Response">
|
||||
{hasFinalResponse ? <div className="message-body" dangerouslySetInnerHTML={{ __html: html }} /> : null}
|
||||
</section>
|
||||
<MessageDebug
|
||||
message={message}
|
||||
codeAgentTimeoutMs={codeAgentTimeoutMs}
|
||||
onCancel={onCancel}
|
||||
onRetry={onRetry}
|
||||
onReplayTrace={onReplayTrace}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<article className={`message-card message-${message.role} tone-border-${toneClass(message.status)}`} data-message-status={message.status}>
|
||||
<header className="message-head">
|
||||
<div>
|
||||
<p className="eyebrow">{message.role === "user" ? "用户" : message.role === "agent" ? "Code Agent" : "System"}</p>
|
||||
<h3>{message.title}</h3>
|
||||
</div>
|
||||
<StateTag tone={statusLabel(message).tone}>{statusLabel(message).label}</StateTag>
|
||||
</header>
|
||||
<div className="message-body" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
<MessageDebug
|
||||
message={message}
|
||||
@@ -136,7 +126,6 @@ function MessageCard({ message, codeAgentTimeoutMs, onCancel, onRetry, onReplayT
|
||||
onRetry={onRetry}
|
||||
onReplayTrace={onReplayTrace}
|
||||
/>
|
||||
{message.runnerTrace ? <MessageTracePanel trace={message.runnerTrace} storageKey={traceKey} /> : null}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -160,6 +149,7 @@ function MessageDebug({ message, codeAgentTimeoutMs, onCancel, onRetry, onReplay
|
||||
return (
|
||||
<>
|
||||
<footer className="message-meta message-meta-compact">
|
||||
<StateTag tone={statusLabel(message).tone}>{statusLabel(message).label}</StateTag>
|
||||
<span>{formatBeijingTime(message.updatedAt ?? message.createdAt, { withSeconds: true })}</span>
|
||||
<MessageActions message={message} onCancel={onCancel} onRetry={onRetry} onReplayTrace={onReplayTrace} />
|
||||
<DebugDialog
|
||||
|
||||
@@ -22,11 +22,20 @@ interface DisplayRow {
|
||||
}
|
||||
|
||||
export function MessageTracePanel({ trace, defaultOpen, storageKey }: MessageTracePanelProps): ReactElement {
|
||||
const storedOpen = readStoredOpen(storageKey, defaultOpen ?? defaultTraceDetailsOpen(trace));
|
||||
const traceRunning = isRunningTrace(trace);
|
||||
const storedOpen = readStoredOpen(storageKey, defaultOpen ?? traceRunning);
|
||||
const [open, setOpen] = useState<boolean>(storedOpen);
|
||||
const [follow, setFollow] = useState<boolean>(true);
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (traceRunning) {
|
||||
setOpen(true);
|
||||
return;
|
||||
}
|
||||
setOpen(false);
|
||||
}, [traceRunning, trace.traceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (storageKey) {
|
||||
try { window.localStorage.setItem(storageKey, open ? "1" : "0"); } catch { /* ignore */ }
|
||||
@@ -118,8 +127,8 @@ function traceCountText(trace: RunnerTrace, events: number, rows: number): strin
|
||||
return `${rows}/${rawTotal} 条事件(已折叠)`;
|
||||
}
|
||||
|
||||
function defaultTraceDetailsOpen(trace: RunnerTrace): boolean {
|
||||
return ["running", "completed", "source", "failed", "timeout", "canceled", "error"].includes(String(trace.status ?? "").toLowerCase());
|
||||
function isRunningTrace(trace: RunnerTrace): boolean {
|
||||
return String(trace.status ?? "").toLowerCase() === "running";
|
||||
}
|
||||
|
||||
function readStoredOpen(key: string | undefined, fallback: boolean): boolean {
|
||||
|
||||
@@ -13,10 +13,9 @@ interface DevicePodSidebarProps {
|
||||
selectedDevicePodId: string;
|
||||
onSelect(devicePodId: string): void;
|
||||
collapsed: boolean;
|
||||
onToggle(): void;
|
||||
}
|
||||
|
||||
export function DevicePodSidebar({ live, selectedDevicePodId, onSelect, collapsed, onToggle }: DevicePodSidebarProps): ReactElement {
|
||||
export function DevicePodSidebar({ live, selectedDevicePodId, onSelect, collapsed }: DevicePodSidebarProps): ReactElement {
|
||||
const [dialog, setDialog] = useState<{ title: string; body: string } | null>(null);
|
||||
const pods = devicePodsFromLive(live);
|
||||
const status = live?.devicePodStatus?.data ?? null;
|
||||
@@ -26,7 +25,6 @@ export function DevicePodSidebar({ live, selectedDevicePodId, onSelect, collapse
|
||||
|
||||
return (
|
||||
<aside className={`right-sidebar${collapsed ? " is-device-pod-collapsed" : ""}`} id="device-pod-sidebar" aria-label="Device Pod 摘要看板" aria-hidden={collapsed}>
|
||||
<button className="right-sidebar-toggle right-sidebar-toggle--anchor" id="right-sidebar-toggle" type="button" aria-controls="device-pod-sidebar" aria-expanded={!collapsed} aria-label={collapsed ? "展开右侧 Device Pod 看板" : "折叠右侧 Device Pod 看板"} onClick={onToggle}>{collapsed ? "展开看板" : "收起看板"}</button>
|
||||
<div className="right-sidebar-content">
|
||||
<WorkbenchProbe live={live} />
|
||||
<WorkbenchBuildSummary live={live} />
|
||||
|
||||
@@ -3,9 +3,7 @@ import type { RouteId } from "../../types/domain";
|
||||
|
||||
interface ActivityRailProps {
|
||||
route: RouteId;
|
||||
collapsed: boolean;
|
||||
onRoute(route: RouteId): void;
|
||||
onToggle(): void;
|
||||
}
|
||||
|
||||
const routes: Array<{ id: RouteId; label: string; title: string }> = [
|
||||
@@ -17,13 +15,12 @@ const routes: Array<{ id: RouteId; label: string; title: string }> = [
|
||||
];
|
||||
const settingsRoute = routes[4];
|
||||
|
||||
export function ActivityRail({ route, collapsed, onRoute, onToggle }: ActivityRailProps): ReactElement {
|
||||
export function ActivityRail({ route, onRoute }: ActivityRailProps): ReactElement {
|
||||
return (
|
||||
<aside className="activity-rail" id="activity-rail" aria-label="工作台活动栏">
|
||||
{routes.slice(0, 4).map((item) => <RailButton key={item.id} item={item} active={route === item.id} onRoute={onRoute} />)}
|
||||
<span className="rail-spacer" aria-hidden="true" />
|
||||
{settingsRoute ? <RailButton item={settingsRoute} active={route === "settings"} onRoute={onRoute} /> : null}
|
||||
<button className="rail-button sidebar-toggle" id="left-sidebar-toggle" type="button" aria-controls="activity-rail" aria-expanded={!collapsed} aria-label={collapsed ? "展开左侧导航" : "折叠左侧导航"} title={collapsed ? "展开左侧导航" : "折叠左侧导航"} onClick={onToggle}>{collapsed ? "展开" : "收起"}</button>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,57 @@
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
import type { ProviderProfile } from "../../types/domain";
|
||||
|
||||
interface SettingsViewProps {
|
||||
providerProfile: ProviderProfile;
|
||||
codeAgentTimeoutMs: number;
|
||||
gatewayShellTimeoutMs: number;
|
||||
onProviderProfile(profile: ProviderProfile): void;
|
||||
onCodeAgentTimeout(value: number): void;
|
||||
onGatewayShellTimeout(value: number): void;
|
||||
onLogout(): Promise<void>;
|
||||
}
|
||||
|
||||
export function SettingsView({ onLogout }: SettingsViewProps): ReactElement {
|
||||
export function SettingsView({ providerProfile, codeAgentTimeoutMs, gatewayShellTimeoutMs, onProviderProfile, onCodeAgentTimeout, onGatewayShellTimeout, onLogout }: SettingsViewProps): ReactElement {
|
||||
return (
|
||||
<section className="view settings-view" id="settings" data-view="settings" aria-labelledby="settings-title">
|
||||
<div className="workspace-panel settings-panel">
|
||||
<div className="panel-title-row"><div><p className="eyebrow">账号</p><h2 id="settings-title">设置</h2></div></div>
|
||||
<div className="settings-content"><button className="logout-button" id="logout-button" type="button" onClick={() => void onLogout()}>注销登录</button></div>
|
||||
<div className="panel-title-row"><div><p className="eyebrow">账号与运行参数</p><h2 id="settings-title">设置</h2></div></div>
|
||||
<div className="settings-content">
|
||||
<section className="settings-group" aria-label="Code Agent 设置">
|
||||
<label className="agent-timeout-control" htmlFor="settings-code-agent-provider-profile">
|
||||
<span>模型通道</span>
|
||||
<select id="settings-code-agent-provider-profile" aria-label="Code Agent 模型通道" value={providerProfile} onChange={(event) => onProviderProfile(event.currentTarget.value as ProviderProfile)}>
|
||||
<option value="deepseek">DeepSeek</option>
|
||||
<option value="codex-api">Codex API</option>
|
||||
<option value="minimax-m3">MiniMax-M3</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="agent-timeout-control" htmlFor="code-agent-timeout">
|
||||
<span>无新事件超时</span>
|
||||
<select id="code-agent-timeout" aria-label="Code Agent 无新事件超时" value={codeAgentTimeoutMs} onChange={(event) => onCodeAgentTimeout(Number(event.currentTarget.value))}>
|
||||
<option value="180000">3 分钟</option>
|
||||
<option value="300000">5 分钟</option>
|
||||
<option value="600000">10 分钟</option>
|
||||
<option value="900000">15 分钟</option>
|
||||
<option value="1200000">20 分钟</option>
|
||||
<option value="1800000">30 分钟</option>
|
||||
<option value="2400000">40 分钟</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="agent-timeout-control" htmlFor="gateway-shell-timeout">
|
||||
<span>Gateway 命令超时</span>
|
||||
<select id="gateway-shell-timeout" aria-label="PC gateway shell 命令超时" value={gatewayShellTimeoutMs} onChange={(event) => onGatewayShellTimeout(Number(event.currentTarget.value))}>
|
||||
<option value="60000">1 分钟</option>
|
||||
<option value="120000">2 分钟</option>
|
||||
<option value="180000">3 分钟</option>
|
||||
<option value="300000">5 分钟</option>
|
||||
<option value="600000">10 分钟</option>
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
<button className="logout-button" id="logout-button" type="button" onClick={() => void onLogout()}>注销登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -40,18 +40,28 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
.login-submit, .logout-button, .command-button { border: 0; border-radius: 6px; padding: 10px 14px; color: #fff; background: var(--accent); font-weight: 800; }
|
||||
.login-error { color: var(--bad); margin: 0; }
|
||||
|
||||
.workbench-shell { height: 100%; width: 100%; display: grid; grid-template-columns: var(--rail-width) var(--session-sidebar-width) var(--resize-handle-width) minmax(420px, 1fr) var(--resize-handle-width) var(--right-sidebar-width); grid-template-rows: minmax(0, 1fr); overflow: hidden; background: var(--panel); }
|
||||
.workbench-shell.is-left-sidebar-collapsed { grid-template-columns: var(--rail-collapsed-width) 0 0 minmax(420px, 1fr) var(--resize-handle-width) var(--right-sidebar-width); }
|
||||
.workbench-shell.is-right-sidebar-collapsed { grid-template-columns: var(--rail-width) var(--session-sidebar-width) var(--resize-handle-width) minmax(420px, 1fr) 0 var(--right-collapsed-width); }
|
||||
.activity-rail { min-width: 0; display: grid; grid-template-rows: repeat(4, minmax(54px, auto)) 1fr minmax(54px, auto) minmax(54px, auto); gap: 6px; padding: 10px 8px; background: var(--rail); overflow-x: hidden; }
|
||||
.workbench-shell { height: 100%; width: 100%; display: grid; grid-template-columns: var(--rail-width) minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); overflow: hidden; background: var(--panel); }
|
||||
.main-workspace { min-width: 0; min-height: 0; display: grid; grid-template-rows: 36px minmax(0, 1fr); overflow: hidden; }
|
||||
.workspace-topbar { min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 4px 8px; border-bottom: 1px solid var(--line); background: #fbfcf8; }
|
||||
.workspace-topbar-button { min-height: 26px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); padding: 3px 8px; font-size: 12px; font-weight: 800; }
|
||||
.workspace-topbar-button:disabled { opacity: 0.35; }
|
||||
.workspace-topbar-status { min-width: 0; display: flex; align-items: center; justify-content: center; gap: 8px; color: var(--muted); font-size: 12px; font-weight: 800; }
|
||||
.workspace-topbar-pill { display: inline-flex; min-height: 22px; align-items: center; border-radius: 999px; padding: 2px 8px; font-size: 11px; font-weight: 800; }
|
||||
.workspace-body { min-width: 0; min-height: 0; display: grid; grid-template-columns: var(--session-sidebar-width) var(--resize-handle-width) minmax(420px, 1fr) var(--resize-handle-width) var(--right-sidebar-width); grid-template-rows: minmax(0, 1fr); overflow: hidden; }
|
||||
.workbench-shell.is-left-sidebar-collapsed .workspace-body { grid-template-columns: 0 0 minmax(420px, 1fr) var(--resize-handle-width) var(--right-sidebar-width); }
|
||||
.workbench-shell.is-right-sidebar-collapsed .workspace-body { grid-template-columns: var(--session-sidebar-width) var(--resize-handle-width) minmax(420px, 1fr) 0 0; }
|
||||
.workbench-shell.is-left-sidebar-collapsed.is-right-sidebar-collapsed .workspace-body { grid-template-columns: 0 0 minmax(420px, 1fr) 0 0; }
|
||||
.workspace-body.is-route-without-sidebars { grid-template-columns: minmax(0, 1fr); }
|
||||
.activity-rail { min-width: 0; display: grid; grid-template-rows: repeat(4, minmax(54px, auto)) 1fr minmax(54px, auto); gap: 6px; padding: 10px 8px; background: var(--rail); overflow-x: hidden; }
|
||||
.rail-button { min-width: 0; min-height: 44px; border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; padding: 8px 6px; background: transparent; color: #eef4ed; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.rail-button.active { color: #1d2b23; background: var(--rail-active); border-color: var(--rail-active); }
|
||||
.rail-spacer { min-height: 8px; }
|
||||
.is-left-sidebar-collapsed .rail-button:not(.sidebar-toggle) { color: transparent; }
|
||||
.is-left-sidebar-collapsed .session-sidebar { display: none; }
|
||||
|
||||
.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-head, .panel-title-row, .right-sidebar-head, .gate-header { min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.panel-title-row { flex-wrap: wrap; }
|
||||
.panel-title-row > div { min-width: 0; }
|
||||
.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; }
|
||||
@@ -70,9 +80,10 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
|
||||
.center-workspace { min-width: 0; min-height: 0; display: grid; grid-template-rows: minmax(0, 1fr) auto; overflow: hidden; }
|
||||
.view { min-width: 0; min-height: 0; overflow: auto; padding: 14px; }
|
||||
.workspace-panel { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 1px 0 rgba(0,0,0,0.03); }
|
||||
.conversation-panel { height: 100%; display: grid; grid-template-rows: auto auto minmax(0, 1fr); gap: 12px; padding: 14px; overflow: hidden; }
|
||||
.panel-title-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.workspace-panel { min-width: 0; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 1px 0 rgba(0,0,0,0.03); }
|
||||
.conversation-column { min-width: 0; min-height: 0; overflow: hidden; }
|
||||
.conversation-panel { height: 100%; display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 10px; padding: 10px; overflow: hidden; }
|
||||
.panel-title-actions { min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
|
||||
.state-tag { display: inline-flex; min-height: 28px; align-items: center; border-radius: 999px; padding: 4px 10px; font-size: 12px; font-weight: 800; white-space: normal; }
|
||||
.tone-ok, .tone-dev-live { color: var(--ok); background: #e4f4ea; }
|
||||
.tone-source { color: var(--source); background: #e7eef2; }
|
||||
@@ -83,15 +94,9 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
.tone-border-pending { border-color: #e4bd68 !important; }
|
||||
.tone-border-blocked, .tone-border-error { border-color: #e7a1a1 !important; }
|
||||
|
||||
.code-agent-summary { border: 1px solid var(--line); border-radius: 6px; padding: 8px 10px; background: #fbfcf8; }
|
||||
.code-agent-summary summary { display: flex; gap: 10px; align-items: center; cursor: pointer; min-width: 0; }
|
||||
.code-agent-summary-detail pre, .message-trace pre, .skill-preview, .help-content, #device-detail-body { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.code-agent-summary-rows { display: grid; gap: 4px 14px; grid-template-columns: minmax(180px, max-content) 1fr; align-items: baseline; }
|
||||
.code-agent-summary-row { display: contents; }
|
||||
.code-agent-summary-key { color: var(--muted); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
.code-agent-summary-value { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; overflow-wrap: anywhere; }
|
||||
.code-agent-summary-raw { margin-top: 8px; font-size: 12px; color: var(--muted); }
|
||||
.code-agent-summary-raw pre { margin: 4px 0 0; max-height: 220px; overflow: auto; background: #f3f5f1; padding: 8px; border-radius: 6px; }
|
||||
.code-agent-debug-summary { display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; min-width: 0; flex-wrap: wrap; }
|
||||
.code-agent-debug-summary .code-agent-summary-label, .code-agent-debug-summary .code-agent-summary-capability { max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
||||
.message-trace pre, .skill-preview, .help-content, #device-detail-body { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.message-trace-toolbar { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); background: #f7f8f4; }
|
||||
.message-trace-toolbar-spacer { flex: 1; }
|
||||
.message-trace-toolbar button { padding: 4px 10px; min-height: 28px; }
|
||||
@@ -152,25 +157,29 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
.gate-raw pre { margin: 4px 0 0; max-height: 240px; overflow: auto; background: #f3f5f1; padding: 8px; border-radius: 6px; }
|
||||
.gate-table tbody tr[data-status-key="blocked"] td:first-child { border-left: 3px solid var(--bad); padding-left: 5px; }
|
||||
.gate-table tbody tr[data-status-key="pass"] td:first-child { border-left: 3px solid var(--ok); padding-left: 5px; }
|
||||
.drafts-list { display: grid; gap: 4px; padding: 6px 14px; border-top: 1px dashed var(--line); background: #f7f8f4; }
|
||||
.drafts-list { position: absolute; right: 92px; bottom: calc(100% + 6px); z-index: 30; width: min(620px, calc(100% - 20px)); display: grid; gap: 4px; padding: 8px; border: 1px solid var(--line); border-radius: 8px; background: #fff; box-shadow: var(--shadow); }
|
||||
.drafts-list-head { display: flex; align-items: center; justify-content: space-between; }
|
||||
.drafts-list-head .command-button { padding: 2px 8px; min-height: 24px; }
|
||||
.drafts-list-items { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.draft-chip { border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); padding: 2px 8px; font-size: 12px; cursor: pointer; max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.draft-chip:hover, .draft-chip:focus-visible { border-color: var(--accent); outline: 0; }
|
||||
.conversation-list { min-height: 0; display: grid; align-content: start; gap: 12px; overflow: auto; padding-right: 4px; }
|
||||
.message-card { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: 12px; }
|
||||
.message-head { display: flex; justify-content: space-between; gap: 10px; }
|
||||
.message-head h3 { margin: 0; font-size: 15px; }
|
||||
.message-body { line-height: 1.58; }
|
||||
.conversation-list { min-height: 0; display: grid; align-content: start; gap: 8px; overflow: auto; padding: 0 4px 2px 0; }
|
||||
.message-card { border: 1px solid var(--line); border-radius: 6px; background: #fff; padding: 8px; }
|
||||
.message-card.message-user { justify-self: end; width: fit-content; max-width: min(74%, 720px); padding: 0; border-color: #c9d7d2; background: #f7fbf8; }
|
||||
.message-card.message-agent { display: grid; gap: 6px; width: 100%; }
|
||||
.message-head { display: none; }
|
||||
.message-body { line-height: 1.45; }
|
||||
.message-user .message-body { padding: 7px 10px; }
|
||||
.message-body p { margin: 0 0 6px; }
|
||||
.message-body p:last-child { margin-bottom: 0; }
|
||||
.message-body pre { overflow: auto; background: #f3f5f1; padding: 10px; border-radius: 6px; }
|
||||
.message-final-response { min-height: 0; }
|
||||
.message-meta { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
|
||||
.message-trace { margin-top: 10px; border-top: 1px solid var(--line); padding-top: 8px; width: 100%; }
|
||||
.message-trace { margin-top: 0; border: 1px solid var(--line); border-radius: 6px; padding-top: 0; width: 100%; overflow: hidden; }
|
||||
.message-trace[data-trace-mode="all"], .message-trace[open] { width: 100%; }
|
||||
.message-trace-events { max-height: min(540px, 60dvh); overflow: auto; display: grid; gap: 8px; width: 100%; }
|
||||
.message-trace > summary { margin: 0; padding: 4px 8px; background: #f7f8f4; cursor: pointer; }
|
||||
.message-trace-events { max-height: min(520px, 54dvh); overflow: auto; display: grid; gap: 8px; width: 100%; }
|
||||
|
||||
.code-agent-summary-wrapper { display: flex; align-items: stretch; gap: 8px; min-width: 0; }
|
||||
.code-agent-summary-wrapper > .code-agent-summary { flex: 1 1 auto; min-width: 0; }
|
||||
.code-agent-summary-debug { flex: 0 0 auto; align-self: center; }
|
||||
.debug-dialog-trigger { border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); padding: 4px 10px; font-size: 12px; font-weight: 700; cursor: pointer; }
|
||||
.debug-dialog-trigger:hover, .debug-dialog-trigger:focus-visible { border-color: var(--accent); outline: 0; }
|
||||
@@ -188,33 +197,34 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
.debug-dialog-key { color: var(--muted); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
.debug-dialog-value { font-size: 12px; overflow-wrap: anywhere; }
|
||||
.debug-dialog-raw { margin: 0; max-height: 240px; overflow: auto; background: #f3f5f1; padding: 8px; border-radius: 6px; font-size: 12px; }
|
||||
.message-meta-compact { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 6px; }
|
||||
.message-meta-compact { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-height: 28px; margin-top: 0; padding-top: 4px; border-top: 1px solid var(--line); }
|
||||
.message-user .message-meta-compact { border-top: 1px solid #e2e9e5; padding: 4px 6px; font-size: 11px; }
|
||||
.message-meta-compact .debug-dialog-trigger { margin-left: auto; }
|
||||
.message-pending-inline { display: flex; align-items: center; gap: 8px; margin: 8px 0 0; padding: 4px 8px; background: #fbfcf8; border-radius: 6px; }
|
||||
.message-pending-inline { display: flex; align-items: center; gap: 8px; margin: 0; padding: 3px 6px; background: #fbfcf8; border-radius: 6px; }
|
||||
.message-pending-inline-text { color: var(--muted); font-size: 12px; }
|
||||
.message-trace { width: 100%; }
|
||||
.message-trace-events { width: 100%; }
|
||||
|
||||
.command-bar { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; grid-template-areas: "input send clear" "profile agent gateway"; gap: 8px; align-items: stretch; padding: 10px 14px; border-top: 1px solid var(--line); background: #f7f8f4; }
|
||||
.command-bar { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; grid-template-areas: "profile drafts send clear" "input input input input"; gap: 6px; align-items: end; padding: 8px 10px; border-top: 1px solid var(--line); background: #f7f8f4; }
|
||||
.command-bar .input-shell { grid-area: input; }
|
||||
.command-bar #command-drafts-toggle { grid-area: drafts; }
|
||||
.command-bar #command-send { grid-area: send; }
|
||||
.command-bar #command-clear { grid-area: clear; }
|
||||
.command-bar .agent-timeout-control[for="code-agent-provider-profile"] { grid-area: profile; }
|
||||
.command-bar .agent-timeout-control[for="code-agent-timeout"] { grid-area: agent; }
|
||||
.command-bar .agent-timeout-control[for="gateway-shell-timeout"] { grid-area: gateway; }
|
||||
.agent-timeout-control { display: grid; gap: 4px; min-width: 126px; font-size: 12px; font-weight: 700; }
|
||||
.agent-timeout-control select, .device-pod-picker select { border: 1px solid var(--line); border-radius: 6px; padding: 8px; background: var(--surface); }
|
||||
.input-shell { min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 8px; border: 1px solid var(--line); border-radius: 8px; padding: 6px 8px; background: #fff; }
|
||||
.agent-timeout-control { display: grid; gap: 3px; min-width: 126px; font-size: 12px; font-weight: 700; }
|
||||
.agent-timeout-control select, .device-pod-picker select { border: 1px solid var(--line); border-radius: 6px; padding: 6px 8px; background: var(--surface); }
|
||||
.input-shell { min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: end; gap: 8px; border: 1px solid var(--line); border-radius: 8px; padding: 5px 8px; background: #fff; }
|
||||
.prompt-mark { color: var(--accent); font-weight: 900; }
|
||||
#command-input { min-width: 0; width: 100%; min-height: 40px; max-height: 120px; resize: none; border: 0; outline: none; }
|
||||
#command-input { min-width: 0; width: 100%; min-height: 30px; max-height: 110px; resize: none; border: 0; outline: none; }
|
||||
|
||||
.right-sidebar { min-width: 0; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 0; padding: 0; background: #fbfcf8; border-left: 1px solid var(--line); overflow: hidden; }
|
||||
.command-bar .command-button { min-height: 32px; padding: 6px 10px; }
|
||||
.command-drafts-toggle { white-space: nowrap; }
|
||||
|
||||
.right-sidebar { min-width: 0; min-height: 0; display: grid; grid-template-rows: minmax(0, 1fr); gap: 0; padding: 0; background: #fbfcf8; border-left: 1px solid var(--line); overflow: hidden; }
|
||||
.right-sidebar-content { min-width: 0; min-height: 0; display: grid; grid-template-rows: auto auto auto auto minmax(0, 1fr); gap: 10px; padding: 14px; overflow-y: hidden; }
|
||||
.right-sidebar-toggle--anchor { margin: 8px; padding: 8px 6px; min-height: 32px; align-self: start; writing-mode: horizontal-tb; }
|
||||
.is-right-sidebar-collapsed .right-sidebar-content { display: none; }
|
||||
.is-right-sidebar-collapsed .right-sidebar-resize { display: none; }
|
||||
.is-right-sidebar-collapsed .right-sidebar { grid-template-rows: auto; padding: 0; }
|
||||
.is-right-sidebar-collapsed .right-sidebar-toggle--anchor { writing-mode: vertical-rl; min-height: 100%; margin: 6px 4px; padding: 10px 4px; letter-spacing: 0.06em; }
|
||||
.is-right-sidebar-collapsed .right-sidebar { display: none; }
|
||||
.device-pod-picker { display: grid; gap: 6px; font-weight: 800; }
|
||||
.device-pod-status { display: grid; gap: 10px; }
|
||||
.device-pod-summary, .device-pod-interfaces { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
||||
@@ -234,6 +244,8 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
.device-detail-dialog form { display: grid; gap: 10px; padding: 16px; }
|
||||
|
||||
.skills-panel, .settings-panel, .help-panel { padding: 14px; }
|
||||
.settings-content { display: grid; align-content: start; gap: 14px; }
|
||||
.settings-group { display: grid; grid-template-columns: repeat(3, minmax(150px, 220px)); gap: 10px; align-items: end; }
|
||||
.skills-layout { min-height: 0; display: grid; grid-template-columns: 280px minmax(220px, 1fr) minmax(260px, 1.2fr); gap: 12px; }
|
||||
.skill-upload-panel, .skill-detail-panel, .skill-preview-panel { min-width: 0; display: grid; align-content: start; gap: 10px; }
|
||||
.skill-upload-form { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
|
||||
@@ -253,30 +265,40 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
.gate-table th, .gate-table td { border-bottom: 1px solid var(--line); padding: 8px; text-align: left; }
|
||||
|
||||
@media (max-width: 1240px) {
|
||||
.workbench-shell, .workbench-shell.is-right-sidebar-collapsed, .workbench-shell.is-left-sidebar-collapsed { grid-template-columns: 72px minmax(180px, 260px) minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) minmax(280px, 38vh); }
|
||||
.activity-rail { grid-column: 1; grid-row: 1 / span 2; }
|
||||
.session-sidebar { grid-column: 2; grid-row: 1 / span 2; }
|
||||
.workbench-shell { grid-template-columns: 72px minmax(0, 1fr); }
|
||||
.workspace-body, .workbench-shell.is-left-sidebar-collapsed .workspace-body, .workbench-shell.is-right-sidebar-collapsed .workspace-body { grid-template-columns: minmax(180px, 260px) minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) minmax(260px, 36vh); }
|
||||
.activity-rail { grid-column: 1; grid-row: 1; }
|
||||
.session-sidebar { grid-column: 1; grid-row: 1 / span 2; }
|
||||
.session-sidebar-resize, .right-sidebar-resize { display: none; }
|
||||
.center-workspace { grid-column: 3; grid-row: 1; }
|
||||
.right-sidebar { grid-column: 3; grid-row: 2; border-left: 0; border-top: 1px solid var(--line); }
|
||||
.center-workspace { grid-column: 2; grid-row: 1; }
|
||||
.right-sidebar { grid-column: 2; grid-row: 2; border-left: 0; border-top: 1px solid var(--line); }
|
||||
.workbench-shell.is-left-sidebar-collapsed .workspace-body, .workspace-body.is-route-without-sidebars { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); }
|
||||
.workbench-shell.is-left-sidebar-collapsed .center-workspace, .workspace-body.is-route-without-sidebars .center-workspace { grid-column: 1; grid-row: 1; }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
html, body, #root { overflow: hidden; }
|
||||
.workbench-shell, .workbench-shell.is-right-sidebar-collapsed, .workbench-shell.is-left-sidebar-collapsed { grid-template-columns: 56px minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) minmax(260px, 34vh); }
|
||||
.activity-rail { grid-column: 1; grid-row: 1 / span 2; padding: 6px 4px; }
|
||||
.workbench-shell { grid-template-columns: 56px minmax(0, 1fr); }
|
||||
.main-workspace { grid-template-rows: 34px minmax(0, 1fr); }
|
||||
.workspace-body, .workbench-shell.is-left-sidebar-collapsed .workspace-body, .workbench-shell.is-right-sidebar-collapsed .workspace-body { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) minmax(220px, 32vh); }
|
||||
.activity-rail { grid-column: 1; grid-row: 1; padding: 6px 4px; }
|
||||
.rail-button { font-size: 11px; min-height: 40px; }
|
||||
.session-sidebar { display: none; }
|
||||
.center-workspace { grid-column: 2; grid-row: 1; }
|
||||
.right-sidebar { grid-column: 2; grid-row: 2; padding: 10px; }
|
||||
.center-workspace { grid-column: 1; grid-row: 1; }
|
||||
.right-sidebar { grid-column: 1; grid-row: 2; padding: 0; }
|
||||
.view { padding: 8px; }
|
||||
.conversation-panel { padding: 10px; }
|
||||
|
||||
.command-bar { grid-template-columns: minmax(0, 1fr) auto auto; grid-template-areas: "input send clear" "profile profile profile" "agent agent agent" "gateway gateway gateway"; align-items: stretch; }
|
||||
.agent-timeout-control { display: none; }
|
||||
.input-shell { grid-column: 1 / span 3; }
|
||||
.conversation-panel .panel-title-actions { width: 100%; justify-content: flex-start; }
|
||||
.code-agent-debug-summary .code-agent-summary-capability { display: none; }
|
||||
.workspace-topbar-button { max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.command-bar { grid-template-columns: minmax(0, 1fr) auto auto; grid-template-areas: "profile drafts drafts" "input input input" "send send clear"; align-items: stretch; }
|
||||
.command-bar .agent-timeout-control { display: none; }
|
||||
.input-shell { grid-column: auto; }
|
||||
.command-drafts-toggle { grid-area: drafts; }
|
||||
.drafts-list { right: 8px; width: calc(100% - 16px); }
|
||||
.command-button { min-height: 40px; }
|
||||
.device-pod-summary, .device-pod-interfaces { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.device-pod-meta { display: none; }
|
||||
.settings-group { grid-template-columns: minmax(0, 1fr); }
|
||||
.skills-layout { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user