fix(chat): pin composer, set reply language, and track model loading
This commit is contained in:
@@ -9,4 +9,4 @@ import { HarnessToolsProvider } from "./components/ToolUIs";
|
||||
import { useHarnessState, useHarnessStore } from "./assistant/HarnessContext";
|
||||
|
||||
type MobileTab = "skills" | "chat" | "events";
|
||||
export function App() { const store = useHarnessStore(); const state = useHarnessState(); const [settings, setSettings] = useState(false); const [tab, setTab] = useState<MobileTab>("chat"); return <HarnessToolsProvider><div className="app-shell"><header className="topbar"><div className="brand"><span className="brand-mark"><Bot/></span><div><strong>Agent Studio Mini</strong><small>Pi Coding Agent Harness</small></div></div><div className="session-summary"><span className={`run-dot ${state.isRunning ? "running" : ""}`}/><span>{state.sessionId?.slice(0, 16) ?? "creating session…"}</span><span className="model-badge">{state.model.modelId ?? "Model missing"}</span>{state.model.supportsImages && <span className="vision-badge">Vision</span>}<button className="icon-button" title="Reset Session" aria-label="重置 Session" disabled={state.isRunning} onClick={() => void store.reset()}><RotateCcw/></button><button className="settings-button" onClick={() => setSettings(true)}><Settings/>Settings</button></div></header><nav className="mobile-tabs" aria-label="移动端面板切换">{(["skills", "chat", "events"] as MobileTab[]).map((item) => <button key={item} className={tab === item ? "active" : ""} onClick={() => setTab(item)}>{item}</button>)}</nav><div className="workspace-grid"><div className={`mobile-panel ${tab === "skills" ? "shown" : ""}`}><SkillsPanel/></div><div className={`mobile-panel center ${tab === "chat" ? "shown" : ""}`}><ChatPanel openSettings={() => setSettings(true)}/></div><div className={`mobile-panel ${tab === "events" ? "shown" : ""}`}><TimelinePanel/></div></div>{settings && <SettingsDialog open onOpenChange={setSettings}/>}<ApprovalDialog/></div></HarnessToolsProvider>; }
|
||||
export function App() { const store = useHarnessStore(); const state = useHarnessState(); const [settings, setSettings] = useState(false); const [tab, setTab] = useState<MobileTab>("chat"); return <HarnessToolsProvider><div className="app-shell"><header className="topbar"><div className="brand"><span className="brand-mark"><Bot/></span><div><strong>Agent Studio Mini</strong><small>Pi Coding Agent Harness</small></div></div><div className="session-summary"><span className={`run-dot ${state.isRunning ? "running" : ""}`}/><span>{state.sessionId?.slice(0, 16) ?? "creating session…"}</span><span className="model-badge">{state.modelLoadState === "loading" ? "正在读取模型…" : state.modelLoadState === "error" ? "模型读取失败" : state.model.modelId ?? "Model missing"}</span>{state.model.supportsImages && <span className="vision-badge">Vision</span>}<button className="icon-button" title="Reset Session" aria-label="重置 Session" disabled={state.isRunning || state.modelLoadState === "loading"} onClick={() => void store.reset()}><RotateCcw/></button><button className="settings-button" onClick={() => setSettings(true)}><Settings/>Settings</button></div></header><nav className="mobile-tabs" aria-label="移动端面板切换">{(["skills", "chat", "events"] as MobileTab[]).map((item) => <button key={item} className={tab === item ? "active" : ""} onClick={() => setTab(item)}>{item}</button>)}</nav><div className="workspace-grid"><div className={`mobile-panel ${tab === "skills" ? "shown" : ""}`}><SkillsPanel/></div><div className={`mobile-panel center ${tab === "chat" ? "shown" : ""}`}><ChatPanel openSettings={() => setSettings(true)}/></div><div className={`mobile-panel ${tab === "events" ? "shown" : ""}`}><TimelinePanel/></div></div>{settings && <SettingsDialog open onOpenChange={setSettings}/>}<ApprovalDialog/></div></HarnessToolsProvider>; }
|
||||
|
||||
@@ -8,7 +8,7 @@ export function HarnessRuntime({ store, children }: PropsWithChildren<{ store: H
|
||||
const runtime = useExternalStoreRuntime({
|
||||
messages: state.messages,
|
||||
isRunning: state.isRunning,
|
||||
isSendDisabled: !state.model.configured || !state.model.keyConfigured || state.connection !== "online",
|
||||
isSendDisabled: state.modelLoadState !== "loaded" || !state.model.configured || !state.model.keyConfigured || state.connection !== "online",
|
||||
setMessages: store.setMessages,
|
||||
onNew: store.onNew,
|
||||
onCancel: store.onCancel,
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface ClientState {
|
||||
events: HarnessEvent[];
|
||||
skills: SkillCatalogItem[];
|
||||
model: RedactedModelConnection;
|
||||
modelLoadState: "loading" | "loaded" | "error";
|
||||
modelLoadError?: string | undefined;
|
||||
isRunning: boolean;
|
||||
connection: ConnectionState;
|
||||
error?: string | undefined;
|
||||
@@ -19,7 +21,7 @@ const eventTypes: HarnessEvent["type"][] = ["session.started", "session.ended",
|
||||
const modelStorageKey = "agent-studio-mini:model-connection:v1";
|
||||
|
||||
export class HarnessClientStore {
|
||||
private state: ClientState = { messages: [], events: [], skills: [], model: { configured: false, source: "missing", keyConfigured: false }, isRunning: false, connection: "connecting" };
|
||||
private state: ClientState = { messages: [], events: [], skills: [], modelLoadState: "loading", model: { configured: false, source: "missing", keyConfigured: false }, isRunning: false, connection: "connecting" };
|
||||
private readonly listeners = new Set<Listener>();
|
||||
private readonly seen = new Set<string>();
|
||||
private readonly attachments = new Map<string, LocalAttachment>();
|
||||
@@ -42,6 +44,7 @@ export class HarnessClientStore {
|
||||
}
|
||||
|
||||
private async initializeOnce(): Promise<void> {
|
||||
this.update({ modelLoadState: "loading", modelLoadError: undefined });
|
||||
try {
|
||||
// Session readiness must not depend on the optional Skills catalog request.
|
||||
const created = await api<{ sessionId: string }>("/api/sessions", { method: "POST" });
|
||||
@@ -51,7 +54,7 @@ export class HarnessClientStore {
|
||||
await this.restorePersistedModel();
|
||||
void this.refreshSkills();
|
||||
} catch (error) {
|
||||
this.update({ connection: "offline", error: `Session 初始化失败:${messageOf(error)}` });
|
||||
this.update({ connection: "offline", modelLoadState: "error", modelLoadError: `Session 初始化失败:${messageOf(error)}`, error: `Session 初始化失败:${messageOf(error)}` });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,20 +158,45 @@ export class HarnessClientStore {
|
||||
|
||||
async reset(): Promise<void> {
|
||||
const old = this.state.sessionId;
|
||||
this.update({ modelLoadState: "loading", modelLoadError: undefined, connection: "connecting" });
|
||||
this.generation++;
|
||||
for (const image of this.reportImages.values()) if (image) URL.revokeObjectURL(image);
|
||||
this.reportImages.clear();
|
||||
this.source?.close();
|
||||
if (old) await api(`/api/sessions/${old}`, { method: "DELETE" });
|
||||
for (const item of this.attachments.values()) URL.revokeObjectURL(item.objectUrl);
|
||||
this.attachments.clear(); this.seen.clear(); this.lastSequence = 0;
|
||||
const created = await api<{ sessionId: string }>("/api/sessions", { method: "POST" });
|
||||
this.state = { ...this.state, sessionId: created.sessionId, messages: [], events: [], model: { configured: false, source: "missing", keyConfigured: false }, isRunning: false, connection: "connecting", error: undefined };
|
||||
for (const listener of this.listeners) listener();
|
||||
await this.refreshModel(); await this.restorePersistedModel(); this.connect();
|
||||
try {
|
||||
for (const image of this.reportImages.values()) if (image) URL.revokeObjectURL(image);
|
||||
this.reportImages.clear();
|
||||
this.source?.close();
|
||||
if (old) await api(`/api/sessions/${old}`, { method: "DELETE" });
|
||||
for (const item of this.attachments.values()) URL.revokeObjectURL(item.objectUrl);
|
||||
this.attachments.clear(); this.seen.clear(); this.lastSequence = 0;
|
||||
const created = await api<{ sessionId: string }>("/api/sessions", { method: "POST" });
|
||||
this.state = { ...this.state, sessionId: created.sessionId, messages: [], events: [], model: { configured: false, source: "missing", keyConfigured: false }, modelLoadState: "loading", modelLoadError: undefined, isRunning: false, connection: "connecting", error: undefined };
|
||||
for (const listener of this.listeners) listener();
|
||||
this.connect();
|
||||
await this.refreshModel(); await this.restorePersistedModel();
|
||||
} catch (error) {
|
||||
this.update({ connection: "offline", modelLoadState: "error", modelLoadError: `Session 重置失败:${messageOf(error)}` });
|
||||
}
|
||||
}
|
||||
async refreshModel(): Promise<void> { if (!this.state.sessionId) return; const model = await api<RedactedModelConnection>(`/api/sessions/${this.state.sessionId}/model-config`); this.update({ model }); }
|
||||
async saveModel(input: unknown): Promise<void> { const sessionId = await this.requireSession(); const parsed = modelConnectionInputSchema.parse(input); const model = await api<RedactedModelConnection>(`/api/sessions/${sessionId}/model-config`, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(parsed) }); persistModel(withoutCredential(parsed)); this.update({ model }); }
|
||||
async refreshModel(): Promise<void> {
|
||||
if (!this.state.sessionId) return;
|
||||
const sessionId = this.state.sessionId;
|
||||
const generation = this.generation;
|
||||
this.update({ modelLoadState: "loading", modelLoadError: undefined });
|
||||
try {
|
||||
const model = await api<RedactedModelConnection>(`/api/sessions/${sessionId}/model-config`);
|
||||
if (generation !== this.generation || sessionId !== this.state.sessionId) return;
|
||||
this.update({ model, modelLoadState: "loaded", modelLoadError: undefined });
|
||||
} catch (error) {
|
||||
if (generation !== this.generation || sessionId !== this.state.sessionId) return;
|
||||
this.update({ modelLoadState: "error", modelLoadError: `服务器模型配置读取失败:${messageOf(error)}` });
|
||||
}
|
||||
}
|
||||
async retryModel(): Promise<void> {
|
||||
if (!this.state.sessionId) { await this.initialize(); return; }
|
||||
if (this.state.connection === "offline") this.connect();
|
||||
await this.refreshModel();
|
||||
await this.restorePersistedModel();
|
||||
}
|
||||
async saveModel(input: unknown): Promise<void> { const sessionId = await this.requireSession(); const parsed = modelConnectionInputSchema.parse(input); const model = await api<RedactedModelConnection>(`/api/sessions/${sessionId}/model-config`, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(parsed) }); persistModel(withoutCredential(parsed)); this.update({ model, modelLoadState: "loaded", modelLoadError: undefined }); }
|
||||
async clearCredentials(): Promise<void> { const sessionId = await this.requireSession(); await api(`/api/sessions/${sessionId}/model-config/credentials`, { method: "DELETE" }); await this.refreshModel(); }
|
||||
forgetPersistedModel(): void { removePersistedModel(); }
|
||||
async testModel(): Promise<string> { const sessionId = await this.requireSession(); const result = await api<{ message: string }>(`/api/sessions/${sessionId}/model-config/test`, { method: "POST" }); return result.message; }
|
||||
@@ -195,7 +223,7 @@ export class HarnessClientStore {
|
||||
};
|
||||
|
||||
private async restorePersistedModel(): Promise<void> {
|
||||
if (!this.state.sessionId || this.state.model.configured) return;
|
||||
if (!this.state.sessionId || this.state.modelLoadState !== "loaded" || this.state.model.configured) return;
|
||||
const persisted = readPersistedModel();
|
||||
if (!persisted) return;
|
||||
try {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AttachmentPrimitive, ComposerPrimitive, MessagePrimitive, ThreadPrimiti
|
||||
import { ImagePlus, Send, Square, X } from "lucide-react";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { ToolFallback } from "./ToolUIs";
|
||||
import { useHarnessState } from "../assistant/HarnessContext";
|
||||
import { useHarnessState, useHarnessStore } from "../assistant/HarnessContext";
|
||||
|
||||
const demoPrompt = "请使用 ads-analysis Skill,分析账户 demo-account 最近 7 天数据,找出异常 Campaign,并输出账户总结、关键指标和 3 条优化建议。最后使用 report-writer Skill 生成一份报告草稿。";
|
||||
|
||||
@@ -27,23 +27,26 @@ function ComposerAttachment() {
|
||||
|
||||
export function ChatPanel({ openSettings }: { openSettings: () => void }) {
|
||||
const state = useHarnessState();
|
||||
const store = useHarnessStore();
|
||||
const aui = useAui();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const modelReady = state.model.configured && state.model.keyConfigured;
|
||||
const modelReady = state.modelLoadState === "loaded" && state.model.configured && state.model.keyConfigured;
|
||||
const fillDemo = () => aui.composer().setText(demoPrompt);
|
||||
return <main className="chat-panel" aria-label="Streaming Chat">
|
||||
<div className="chat-toolbar"><div><strong>{state.isRunning ? "Agent 正在运行" : "准备就绪"}</strong><span className={`connection ${state.connection}`}>{state.connection}</span></div><button className="text-button" onClick={fillDemo}>填入 Demo Prompt</button></div>
|
||||
{!modelReady && <button className="config-banner" onClick={openSettings}>{state.model.configured ? "模型配置已恢复,请在 Settings 重新输入 API Key。" : "尚未配置模型。打开 Settings 后才能发送消息。"}</button>}
|
||||
{state.error && <div className="error-banner" role="alert">{state.error}</div>}
|
||||
<ThreadPrimitive.Root className="thread-root"><ThreadPrimitive.Viewport className="thread-viewport"><ThreadPrimitive.Empty><div className="empty-state"><div className="orb">π</div><h2>Agent Loop 从这里开始</h2><p>选择 Demo Prompt,观察 Skill 按需加载、Tool 事件和人工审批。</p></div></ThreadPrimitive.Empty><ThreadPrimitive.Messages components={{ UserMessage, AssistantMessage }}/><ThreadPrimitive.ViewportFooter className="composer-footer">
|
||||
{state.modelLoadState === "loading" && <div className="model-loading-banner" role="status">正在读取服务器模型配置…</div>}
|
||||
{state.modelLoadState === "error" && <div className="error-banner" role="alert">{state.modelLoadError ?? "服务器模型配置读取失败"}<button className="text-button" onClick={() => void store.retryModel()}>重试读取模型配置</button></div>}
|
||||
{state.modelLoadState === "loaded" && !modelReady && <button className="config-banner" onClick={openSettings}>{state.model.configured ? "模型已配置,但缺少 API Key。请在 Settings 输入 API Key。" : "尚未配置模型。打开 Settings 后才能发送消息。"}</button>}
|
||||
{state.error && state.error !== state.modelLoadError && <div className="error-banner" role="alert">{state.error}</div>}
|
||||
<ThreadPrimitive.Root className="thread-root"><ThreadPrimitive.Viewport className="thread-viewport"><ThreadPrimitive.Empty><div className="empty-state"><div className="orb">π</div><h2>Agent Loop 从这里开始</h2><p>选择 Demo Prompt,观察 Skill 按需加载、Tool 事件和人工审批。</p></div></ThreadPrimitive.Empty><ThreadPrimitive.Messages components={{ UserMessage, AssistantMessage }}/></ThreadPrimitive.Viewport><div className="composer-footer">
|
||||
<ComposerPrimitive.Root className={`composer ${dragging ? "dragging" : ""}`}>
|
||||
<ComposerPrimitive.AttachmentDropzone className="dropzone" onDragEnter={() => setDragging(true)} onDragLeave={() => setDragging(false)} onDrop={() => setDragging(false)}>
|
||||
<ComposerPrimitive.Attachments components={{ Attachment: ComposerAttachment }}/>
|
||||
<ComposerPrimitive.Input className="composer-input" placeholder={modelReady ? "给 Agent Studio Mini 发送任务…" : state.model.configured ? "请重新输入 API Key" : "请先配置模型连接"} aria-label="消息输入"/>
|
||||
<ComposerPrimitive.Input className="composer-input" placeholder={state.modelLoadState === "loading" ? "正在读取服务器模型配置…" : state.modelLoadState === "error" ? "模型配置读取失败,请重试" : modelReady ? "给 Agent Studio Mini 发送任务…" : state.model.configured ? "请重新输入 API Key" : "请先配置模型连接"} aria-label="消息输入"/>
|
||||
<div className="composer-actions"><ComposerPrimitive.AddAttachment className="icon-button" aria-label="添加图片" title="选择、拖拽或粘贴 JPEG/PNG/WebP"><ImagePlus/></ComposerPrimitive.AddAttachment><span className="composer-hint">最多 4 张 · 5 MiB/张</span>{state.isRunning ? <ComposerPrimitive.Cancel className="send-button stop" aria-label="停止运行"><Square size={16}/>停止</ComposerPrimitive.Cancel> : <ComposerPrimitive.Send className="send-button" aria-label="发送消息"><Send size={17}/>发送</ComposerPrimitive.Send>}</div>
|
||||
</ComposerPrimitive.AttachmentDropzone>
|
||||
</ComposerPrimitive.Root>
|
||||
</ThreadPrimitive.ViewportFooter></ThreadPrimitive.Viewport></ThreadPrimitive.Root>
|
||||
</div></ThreadPrimitive.Root>
|
||||
<div className="sr-live" aria-live="polite">{state.isRunning ? "" : state.messages.at(-1)?.role === "assistant" ? "Assistant 回复已完成" : ""}</div>
|
||||
</main>;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ button, input, select, textarea { font: inherit; }
|
||||
button { color: inherit; }
|
||||
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
||||
button:disabled { opacity: .46; cursor: not-allowed; }
|
||||
.app-shell { display: grid; grid-template-rows: 64px 1fr; height: 100%; }
|
||||
.app-shell { display: grid; grid-template-rows: 64px minmax(0, 1fr); height: 100%; height: 100dvh; }
|
||||
.topbar { min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 16px; border-bottom: 1px solid var(--border); background: rgb(12 13 15 / .9); backdrop-filter: blur(14px); z-index: 10; }
|
||||
.brand, .session-summary, .brand > div, .chat-toolbar > div { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||
.brand > div { align-items: flex-start; flex-direction: column; gap: 1px; }
|
||||
@@ -52,14 +52,17 @@ button:disabled { opacity: .46; cursor: not-allowed; }
|
||||
.diagnostic.valid { color: var(--cyan); border-color: #267a7a; }.diagnostic.warning { color: #ffd166; border-color: #806c2d; }.diagnostic.error { color: var(--pink); border-color: #8d2940; }
|
||||
.diagnostic-message { color: #ffb4c4; font-size: 11px; margin-top: 8px; }
|
||||
.panel-state { padding: 20px; color: var(--muted); font-size: 13px; text-align: center; }
|
||||
.chat-panel { height: 100%; min-height: 0; display: grid; grid-template-rows: auto auto auto 1fr; background: var(--surface-0); }
|
||||
.chat-panel { height: 100%; min-height: 0; display: flex; flex-direction: column; overflow: hidden; background: var(--surface-0); }
|
||||
.chat-toolbar { min-height: 50px; display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; border-bottom: 1px solid var(--border); font-size: 12px; }
|
||||
.connection { text-transform: capitalize; color: var(--muted); }.connection.online { color: var(--cyan); }.connection.reconnecting { color: #ffd166; }.connection.offline { color: var(--pink); }
|
||||
.text-button { min-height: 32px; color: var(--cyan); }
|
||||
.config-banner, .error-banner { margin: 10px 16px 0; border-radius: 8px; padding: 10px 12px; text-align: left; }
|
||||
.config-banner, .error-banner, .model-loading-banner { flex-shrink: 0; margin: 10px 16px 0; border-radius: 8px; padding: 10px 12px; text-align: left; }
|
||||
.config-banner { border: 1px solid #756326; background: #28230f; color: #ffe395; cursor: pointer; }.error-banner { border: 1px solid #7e293b; background: #2b1118; color: #ffbdca; }
|
||||
.thread-root, .thread-viewport { min-height: 0; height: 100%; }
|
||||
.thread-viewport { overflow-y: auto; padding: 16px 0 0; scroll-padding-bottom: 170px; }
|
||||
.chat-toolbar { flex-shrink: 0; }
|
||||
.model-loading-banner { color: var(--muted); background: var(--surface-1); }
|
||||
.error-banner .text-button { margin-left: 12px; }
|
||||
.thread-root { flex: 1; min-height: 0; display: grid; grid-template-rows: minmax(0, 1fr) auto; }
|
||||
.thread-viewport { min-height: 0; overflow-y: auto; padding: 16px 0; scroll-padding-bottom: 16px; }
|
||||
.empty-state { min-height: 52vh; display: grid; place-items: center; align-content: center; text-align: center; padding: 32px; color: var(--muted); }
|
||||
.empty-state h2 { margin: 16px 0 6px; color: var(--text); font-size: 22px; }.empty-state p { max-width: 460px; line-height: 1.55; }
|
||||
.orb { width: 70px; height: 70px; display: grid; place-items: center; border-radius: 50%; color: var(--cyan); font: 700 28px Georgia, serif; background: radial-gradient(circle at 35% 30%, #27535a, #121c20 62%); border: 1px solid #2e6f74; box-shadow: 0 0 50px rgb(37 244 238 / .12); }
|
||||
@@ -78,7 +81,7 @@ button:disabled { opacity: .46; cursor: not-allowed; }
|
||||
.metrics { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--border); }.metrics div { padding: 10px; border-right: 1px solid var(--border); }.metrics span, .metrics strong { display: block; }.metrics span { color: var(--muted); font-size: 10px; }.metrics strong { margin-top: 4px; font-size: 14px; }
|
||||
.approval-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--border); }
|
||||
.accent { color: #071011; background: var(--cyan); border-color: var(--cyan); font-weight: 750; }.accent:hover { background: #66fffa !important; }.danger { color: #ffb3c3; border-color: #7e2d40; background: #291319; }.danger:hover { border-color: var(--pink) !important; background: #36151e !important; }
|
||||
.composer-footer { position: sticky; bottom: 0; padding: 16px; background: linear-gradient(transparent, var(--surface-0) 18%); }
|
||||
.composer-footer { padding: 16px; padding-bottom: max(16px, env(safe-area-inset-bottom)); background: var(--surface-0); }
|
||||
.composer { width: min(780px, 100%); margin: 0 auto; border: 1px solid var(--border-strong); border-radius: 14px; background: var(--surface-1); box-shadow: var(--shadow); overflow: hidden; }
|
||||
.composer.dragging { border-color: var(--cyan); box-shadow: 0 0 0 3px rgb(37 244 238 / .12); }
|
||||
.dropzone { padding: 8px; }
|
||||
@@ -101,7 +104,7 @@ button:disabled { opacity: .46; cursor: not-allowed; }
|
||||
@keyframes pulse { 50% { opacity: .45; transform: scale(.8); } } @keyframes fade { from { opacity: 0; } }
|
||||
@media (max-width: 1050px) { .workspace-grid { grid-template-columns: 230px minmax(380px, 1fr); }.workspace-grid > .mobile-panel:last-child { display: none; }.session-summary > span:nth-child(2) { display: none; } }
|
||||
@media (max-width: 720px) {
|
||||
html, body, #root { overflow: hidden; }.app-shell { grid-template-rows: 58px 44px 1fr; }.brand small, .model-badge, .vision-badge, .session-summary > .run-dot { display: none; }.topbar { padding: 0 10px; }.settings-button { width: 42px; padding: 0; font-size: 0; }.settings-button svg { width: 18px; }.mobile-tabs { display: grid; grid-template-columns: repeat(3, 1fr); border-bottom: 1px solid var(--border); background: var(--surface-1); }.mobile-tabs button { border: 0; background: transparent; color: var(--muted); text-transform: capitalize; }.mobile-tabs button.active { color: var(--cyan); box-shadow: inset 0 -2px var(--cyan); }.workspace-grid { display: block; min-height: 0; }.mobile-panel { display: none !important; height: 100%; }.mobile-panel.shown { display: block !important; }.side-panel { border: 0; }.chat-toolbar { padding: 6px 10px; }.text-button { padding: 0 8px; }.composer-footer { padding: 10px; }.message { width: calc(100% - 20px); }.metrics { grid-template-columns: repeat(2, 1fr); }.form-grid { grid-template-columns: 1fr; }.span-2 { grid-column: span 1 !important; }.presets { overflow-x: auto; }.presets button { flex: 0 0 auto; }.dialog-actions { flex-wrap: wrap; }.dialog-actions button { flex: 1; }.dialog-actions .danger { margin: 0; }.composer-hint { display: none; }
|
||||
html, body, #root { overflow: hidden; }.app-shell { grid-template-rows: 58px 44px minmax(0, 1fr); }.brand small, .model-badge, .vision-badge, .session-summary > .run-dot { display: none; }.topbar { padding: 0 10px; }.settings-button { width: 42px; padding: 0; font-size: 0; }.settings-button svg { width: 18px; }.mobile-tabs { display: grid; grid-template-columns: repeat(3, 1fr); border-bottom: 1px solid var(--border); background: var(--surface-1); }.mobile-tabs button { border: 0; background: transparent; color: var(--muted); text-transform: capitalize; }.mobile-tabs button.active { color: var(--cyan); box-shadow: inset 0 -2px var(--cyan); }.workspace-grid { display: block; min-height: 0; }.mobile-panel { display: none !important; height: 100%; }.mobile-panel.shown { display: block !important; }.side-panel { border: 0; }.chat-toolbar { padding: 6px 10px; }.text-button { padding: 0 8px; }.composer-footer { padding: 10px; padding-bottom: max(10px, env(safe-area-inset-bottom)); }.message { width: calc(100% - 20px); }.metrics { grid-template-columns: repeat(2, 1fr); }.form-grid { grid-template-columns: 1fr; }.span-2 { grid-column: span 1 !important; }.presets { overflow-x: auto; }.presets button { flex: 0 0 auto; }.dialog-actions { flex-wrap: wrap; }.dialog-actions button { flex: 1; }.dialog-actions .danger { margin: 0; }.composer-hint { display: none; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; } }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user