Merge pull request #822 from pikasTech/fix/issue-816-ui-density

fix: compact v0.2 code agent workbench layout
This commit is contained in:
Lyon
2026-06-04 15:33:59 +08:00
committed by GitHub
9 changed files with 253 additions and 184 deletions
+84 -54
View File
@@ -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>
);
+76 -54
View File
@@ -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); }
}