Replace repository with DuckLM runtime

This commit is contained in:
2026-05-20 01:00:28 +08:00
parent ddc285b8f4
commit 4a84ada770
190 changed files with 7060 additions and 13602 deletions
+510
View File
@@ -0,0 +1,510 @@
const state = {
running: false,
messages: [],
};
async function jsonFetch(url, options) {
const response = await fetch(url, options);
if (!response.ok) throw new Error(await response.text());
return response.json();
}
function escapeText(value) {
return String(value ?? "");
}
function setStatus(id, text, tone = "neutral") {
const node = document.querySelector(id);
if (!node) return;
node.textContent = text;
node.dataset.tone = tone;
}
function addMessage(role, content, meta = "", options = {}) {
const list = document.querySelector("#messages");
if (!list) return;
const article = document.createElement("article");
article.className = `message ${role}`;
const avatar = document.createElement("div");
avatar.className = "avatar";
avatar.textContent = role === "user" ? "U" : "D";
const bubble = document.createElement("div");
bubble.className = "bubble";
const messageMeta = document.createElement("div");
messageMeta.className = "message-meta";
messageMeta.innerHTML = `<strong>${role === "user" ? "You" : "DuckLM"}</strong><span>${escapeText(meta)}</span>`;
const text = document.createElement("p");
text.textContent = content;
bubble.append(messageMeta);
if (role === "assistant" && options.reasoning) {
bubble.append(createInlineReasoning());
}
bubble.append(text);
article.append(avatar, bubble);
list.append(article);
list.scrollTop = list.scrollHeight;
return article;
}
function createInlineReasoning() {
const section = document.createElement("section");
section.className = "message-reasoning is-collapsed";
const button = document.createElement("button");
button.className = "message-reasoning-toggle";
button.type = "button";
button.setAttribute("aria-expanded", "false");
const title = document.createElement("span");
title.textContent = "Размышление";
const status = document.createElement("span");
status.className = "message-reasoning-status";
status.textContent = "streaming";
button.append(title, status);
const body = document.createElement("pre");
body.hidden = true;
body.textContent = "";
section.append(button, body);
return section;
}
function createToolTerminal(eventPayload) {
const payload = eventPayload.payload || eventPayload;
const args = payload.args || {};
const terminal = document.createElement("section");
terminal.className = "tool-terminal";
terminal.dataset.toolIndex = String(payload.index || "");
const header = document.createElement("div");
header.className = "tool-terminal-header";
const dots = document.createElement("span");
dots.className = "terminal-dots";
dots.innerHTML = "<i></i><i></i><i></i>";
const title = document.createElement("span");
title.className = "tool-terminal-title";
title.textContent = formatToolCommand(payload.tool, args);
const status = document.createElement("span");
status.className = "tool-terminal-status";
status.textContent = "running";
header.append(dots, title, status);
const body = document.createElement("pre");
body.className = "tool-terminal-body";
body.textContent = formatToolStart(payload.tool, args);
terminal.append(header, body);
return terminal;
}
function formatToolCommand(tool, args) {
if (tool === "shell_exec_safe") return `$ ${args.command || tool}`;
if (tool === "file_read") return `$ file_read ${args.path || ""}`.trim();
if (tool === "file_write") return `$ file_write ${args.path || ""}`.trim();
return `$ ${tool || "tool"}`;
}
function formatToolStart(tool, args) {
const lines = [formatToolCommand(tool, args)];
const serializedArgs = JSON.stringify(args || {}, null, 2);
if (serializedArgs !== "{}") lines.push(serializedArgs);
return lines.join("\n");
}
function appendToolTerminal(article, eventPayload) {
const paragraph = article?.querySelector("p");
const terminal = createToolTerminal(eventPayload);
paragraph?.before(terminal);
document.querySelector("#messages").scrollTop = document.querySelector("#messages").scrollHeight;
}
function updateToolTerminal(article, eventPayload) {
const payload = eventPayload.payload || eventPayload;
const terminal = article?.querySelector(`.tool-terminal[data-tool-index="${payload.index || ""}"]`);
const body = terminal?.querySelector(".tool-terminal-body");
const status = terminal?.querySelector(".tool-terminal-status");
const result = payload.result || {};
if (!body || !status) return;
terminal.classList.toggle("is-error", !result.ok);
status.textContent = result.ok ? "ok" : "error";
const parts = [body.textContent.trim()];
if (result.output) parts.push("\nstdout\n" + result.output.trimEnd());
if (result.error) parts.push("\nstderr\n" + result.error.trimEnd());
if (result.metadata && Object.keys(result.metadata).length) {
parts.push("\nmetadata\n" + JSON.stringify(result.metadata, null, 2));
}
body.textContent = parts.join("\n");
document.querySelector("#messages").scrollTop = document.querySelector("#messages").scrollHeight;
}
function appendApprovalTerminal(article, eventPayload) {
const payload = eventPayload.payload || eventPayload;
appendToolTerminal(article, {
payload: {
index: payload.index,
tool: payload.tool,
args: payload.action?.args || {},
},
});
const terminal = article?.querySelector(`.tool-terminal[data-tool-index="${payload.index || ""}"]`);
const body = terminal?.querySelector(".tool-terminal-body");
const status = terminal?.querySelector(".tool-terminal-status");
terminal?.classList.add("is-waiting");
if (status) status.textContent = "approval";
if (body) body.textContent += `\n\napproval required\n${payload.reason || ""}`;
}
function setMessagePending(article, text) {
const paragraph = article?.querySelector("p");
if (paragraph) paragraph.textContent = text;
}
function appendMessageText(article, delta) {
const paragraph = article?.querySelector("p");
if (!paragraph) return;
paragraph.textContent += delta;
document.querySelector("#messages").scrollTop = document.querySelector("#messages").scrollHeight;
}
function appendInlineReasoning(article, delta) {
const block = article?.querySelector(".message-reasoning");
const body = block?.querySelector("pre");
const status = block?.querySelector(".message-reasoning-status");
if (!body) return;
body.textContent += delta;
if (status) status.textContent = "streaming";
document.querySelector("#messages").scrollTop = document.querySelector("#messages").scrollHeight;
}
function finishInlineReasoning(article, reasoning) {
const block = article?.querySelector(".message-reasoning");
const body = block?.querySelector("pre");
const status = block?.querySelector(".message-reasoning-status");
if (!body) return;
body.textContent = reasoning?.trim() || body.textContent.trim() || "Размышления не были получены.";
if (status) status.textContent = "done";
}
async function refreshEvents(taskId) {
const events = await jsonFetch(`/v1/tasks/${taskId}/events`);
const list = document.querySelector("#events");
if (!list) return events;
list.innerHTML = "";
for (const event of events) {
const item = document.createElement("li");
const title = document.createElement("strong");
const detail = document.createElement("span");
title.textContent = `${event.sequence}. ${event.event_type}`;
detail.textContent = summarizeEvent(event.payload);
item.append(title, detail);
list.appendChild(item);
}
return events;
}
function summarizeEvent(payload) {
if (!payload || typeof payload !== "object") return "";
if (payload.role && payload.latency_ms) {
return `${payload.role} · ${Math.round(payload.latency_ms)} ms`;
}
if (payload.content) {
return payload.content.slice(0, 140);
}
if (payload.final_response) {
return payload.final_response.slice(0, 140);
}
if (payload.error) {
return payload.error;
}
return JSON.stringify(payload);
}
function toggleInlineReasoning(button) {
const block = button.closest(".message-reasoning");
const body = block?.querySelector("pre");
if (!block || !body) return;
const expanded = button.getAttribute("aria-expanded") === "true";
button.setAttribute("aria-expanded", String(!expanded));
body.hidden = expanded;
block.classList.toggle("is-collapsed", expanded);
}
function parseSseBlock(block) {
const event = {name: "message", data: ""};
for (const line of block.split("\n")) {
if (line.startsWith("event:")) event.name = line.slice(6).trim();
if (line.startsWith("data:")) event.data += line.slice(5).trimStart();
}
if (!event.data) return null;
return {name: event.name, data: JSON.parse(event.data)};
}
async function streamChat(payload, onEvent) {
const response = await fetch("/v1/chat/stream", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(payload),
});
if (!response.ok) throw new Error(await response.text());
if (!response.body) throw new Error("Streaming response is not available in this browser.");
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const {value, done} = await reader.read();
if (done) break;
buffer += decoder.decode(value, {stream: true});
const blocks = buffer.split("\n\n");
buffer = blocks.pop() || "";
for (const block of blocks) {
const event = parseSseBlock(block);
if (event) await onEvent(event);
}
}
buffer += decoder.decode();
if (buffer.trim()) {
const event = parseSseBlock(buffer);
if (event) await onEvent(event);
}
}
async function sendMessage() {
if (state.running) return;
const input = document.querySelector("#message");
const message = input.value.trim();
if (!message) return;
state.running = true;
document.querySelector("#run").disabled = true;
setStatus("#task-status", "running", "warn");
addMessage("user", message, "submitted");
input.value = "";
const pending = addMessage("assistant", "", "thinking", {reasoning: true});
let taskId = "";
let contentStarted = false;
try {
await streamChat({
message,
workspace: document.querySelector("#workspace").value,
debug: document.querySelector("#debug").checked,
}, async ({name, data}) => {
if (data.task_id) taskId = data.task_id;
if (name === "task_created") {
taskId = data.task_id;
setStatus("#task-status", taskId, "warn");
return;
}
if (name === "reasoning_delta") {
pending.querySelector(".message-meta span").textContent = "reasoning";
appendInlineReasoning(pending, data.delta || "");
return;
}
if (name === "tool_call_started") {
pending.querySelector(".message-meta span").textContent = "tool";
appendToolTerminal(pending, data);
return;
}
if (name === "tool_call_finished") {
pending.querySelector(".message-meta span").textContent = "tool";
updateToolTerminal(pending, data);
return;
}
if (name === "tool_approval_requested") {
pending.querySelector(".message-meta span").textContent = "approval";
appendApprovalTerminal(pending, data);
return;
}
if (name === "content_delta") {
if (!contentStarted) {
contentStarted = true;
setMessagePending(pending, "");
}
pending.querySelector(".message-meta span").textContent = "answering";
appendMessageText(pending, data.delta || "");
return;
}
if (name === "done") {
if (!contentStarted) {
setMessagePending(pending, data.final_response || "No final content returned.");
}
pending.querySelector(".message-meta span").textContent = data.status;
setStatus("#task-status", data.task_id, data.status === "completed" ? "ok" : "warn");
finishInlineReasoning(pending, data.reasoning_content);
await refreshEvents(data.task_id);
return;
}
if (name === "error") {
throw new Error(data.error || "Stream failed.");
}
});
} catch (error) {
if (!taskId) input.value = message;
setMessagePending(pending, error.message);
pending.querySelector(".message-meta span").textContent = "failed";
setStatus("#task-status", "failed", "bad");
if (taskId) await refreshEvents(taskId);
} finally {
state.running = false;
document.querySelector("#run").disabled = false;
input.focus();
}
}
async function checkRuntime() {
try {
await jsonFetch("/health");
setStatus("#api-status", "online", "ok");
} catch {
setStatus("#api-status", "offline", "bad");
}
try {
const roles = await jsonFetch("/v1/models/ping");
const ok = Object.values(roles).every((item) => item.ok);
setStatus("#model-status", ok ? "online" : "degraded", ok ? "ok" : "warn");
} catch {
setStatus("#model-status", "offline", "bad");
}
}
function bindChat() {
const composer = document.querySelector("#composer");
const input = document.querySelector("#message");
composer?.addEventListener("submit", (event) => {
event.preventDefault();
sendMessage();
});
input?.addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
});
document.querySelector("#new-chat")?.addEventListener("click", () => {
const messages = document.querySelector("#messages");
messages.innerHTML = "";
addMessage("assistant", "Новая сессия готова.", "ready");
document.querySelector("#events").innerHTML = "";
setStatus("#task-status", "none");
});
document.querySelector("#messages")?.addEventListener("click", (event) => {
const button = event.target.closest(".message-reasoning-toggle");
if (button) toggleInlineReasoning(button);
});
document.querySelector("#debug")?.addEventListener("change", (event) => {
document.querySelector("#debug-panel").hidden = !event.target.checked;
});
}
async function loadSimplePages() {
const skills = document.querySelector("#skills");
if (skills) skills.textContent = JSON.stringify(await jsonFetch("/v1/skills"), null, 2);
const experience = document.querySelector("#experience");
if (experience) experience.textContent = JSON.stringify(await jsonFetch("/v1/experience"), null, 2);
const approvals = document.querySelector("#approvals");
if (approvals) await renderApprovals(approvals);
}
async function renderApprovals(container) {
const approvals = await jsonFetch("/v1/approvals/pending");
container.innerHTML = "";
if (!approvals.length) {
const empty = document.createElement("p");
empty.className = "empty-state";
empty.textContent = "No pending approvals.";
container.append(empty);
return;
}
for (const approval of approvals) {
const card = document.createElement("article");
card.className = "approval-card";
card.dataset.approvalId = approval.approval_id;
const header = document.createElement("div");
header.className = "approval-card-header";
const title = document.createElement("h2");
title.textContent = approval.normalized_action?.tool || "Tool action";
const status = document.createElement("span");
status.textContent = approval.status;
header.append(title, status);
const meta = document.createElement("dl");
meta.className = "approval-meta";
meta.append(metaRow("Task", approval.task_id));
meta.append(metaRow("Approval", approval.approval_id));
meta.append(metaRow("Created", approval.created_at));
const action = document.createElement("pre");
action.className = "approval-action";
action.textContent = JSON.stringify(approval.normalized_action, null, 2);
const actions = document.createElement("div");
actions.className = "approval-actions";
actions.append(
approvalButton("Allow once", "allow_once"),
approvalButton("Allow forever", "allow_forever"),
approvalButton("Deny", "deny", "danger"),
);
card.append(header, meta, action, actions);
container.append(card);
}
}
function metaRow(label, value) {
const row = document.createElement("div");
const dt = document.createElement("dt");
const dd = document.createElement("dd");
dt.textContent = label;
dd.textContent = value || "";
row.append(dt, dd);
return row;
}
function approvalButton(label, action, tone = "") {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
button.dataset.approvalAction = action;
if (tone) button.dataset.tone = tone;
return button;
}
document.querySelector("#approvals")?.addEventListener("click", async (event) => {
const button = event.target.closest("[data-approval-action]");
if (!button) return;
const card = button.closest(".approval-card");
const approvalId = card?.dataset.approvalId;
if (!approvalId) return;
button.disabled = true;
const action = button.dataset.approvalAction;
await jsonFetch(`/v1/approvals/${approvalId}/${action}`, {method: "POST"});
await renderApprovals(document.querySelector("#approvals"));
});
document.querySelector("#memory-search")?.addEventListener("click", async () => {
const q = document.querySelector("#memory-query").value;
document.querySelector("#memory-results").textContent =
JSON.stringify(await jsonFetch(`/v1/memory/search?q=${encodeURIComponent(q)}`), null, 2);
});
bindChat();
checkRuntime();
loadSimplePages().catch(console.error);
+673
View File
@@ -0,0 +1,673 @@
:root {
color-scheme: light;
--bg: #eef2f6;
--sidebar: #111827;
--sidebar-soft: #1f2937;
--panel: #ffffff;
--panel-strong: #f8fafc;
--text: #111827;
--muted: #64748b;
--border: #d7dee8;
--accent: #1f6feb;
--accent-strong: #174ea6;
--ok: #12805c;
--warn: #b7791f;
--bad: #b42318;
--shadow: 0 18px 50px rgba(15, 23, 42, 0.14);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
}
.simple-page {
max-width: 980px;
margin: 0 auto;
padding: 28px;
}
.simple-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
.simple-header h1,
.simple-header p {
margin: 0;
}
.simple-header h1 {
font-size: 24px;
}
.simple-header p {
margin-top: 4px;
color: var(--muted);
}
.approval-list {
display: grid;
gap: 14px;
}
.approval-card {
display: grid;
gap: 14px;
padding: 16px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow);
}
.approval-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.approval-card h2 {
margin: 0;
font-size: 17px;
}
.approval-card-header span {
padding: 3px 8px;
border-radius: 999px;
background: #fef3c7;
color: #854d0e;
font-size: 12px;
font-weight: 800;
}
.approval-meta {
display: grid;
gap: 6px;
}
.approval-meta div {
justify-content: flex-start;
}
.approval-meta dd {
max-width: none;
color: var(--text);
}
.approval-action {
margin: 0;
max-height: 220px;
overflow: auto;
padding: 12px;
background: #0f172a;
border-radius: 8px;
color: #d1fae5;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
line-height: 1.5;
}
.approval-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.approval-actions button {
border: 0;
border-radius: 8px;
padding: 9px 12px;
background: var(--accent);
color: #ffffff;
font-weight: 750;
}
.approval-actions button[data-tone="danger"] {
background: var(--bad);
}
.approval-actions button:disabled {
cursor: wait;
opacity: 0.65;
}
.empty-state {
margin: 0;
padding: 16px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--muted);
}
button, input, textarea {
font: inherit;
}
button {
cursor: pointer;
}
.app-shell {
display: grid;
grid-template-columns: 292px minmax(0, 1fr);
min-height: 100vh;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 18px;
min-height: 100vh;
padding: 22px;
background: var(--sidebar);
color: #e5edf7;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255,255,255,0.12);
}
.brand-mark, .avatar {
display: grid;
place-items: center;
width: 36px;
height: 36px;
border-radius: 8px;
font-weight: 800;
}
.brand-mark {
background: #f8fafc;
color: #111827;
}
.brand h1, .brand p,
.chat-header h2, .chat-header p,
.settings-panel h2, .status-panel h2 {
margin: 0;
}
.brand h1 {
font-size: 18px;
line-height: 1.2;
}
.brand p {
margin-top: 2px;
color: #9ca3af;
font-size: 12px;
}
.side-nav {
display: grid;
gap: 6px;
}
.side-nav a {
color: #cbd5e1;
text-decoration: none;
padding: 10px 12px;
border-radius: 7px;
font-size: 14px;
}
.side-nav a:hover,
.side-nav a.active {
background: var(--sidebar-soft);
color: #ffffff;
}
.settings-panel,
.status-panel {
display: grid;
gap: 12px;
padding: 14px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.10);
border-radius: 8px;
}
.settings-panel h2,
.status-panel h2 {
font-size: 13px;
color: #f8fafc;
}
label {
display: grid;
gap: 7px;
font-size: 13px;
font-weight: 650;
}
.toggle-row {
grid-template-columns: auto 1fr;
align-items: center;
font-weight: 500;
color: #cbd5e1;
}
input,
textarea {
width: 100%;
border: 1px solid var(--border);
border-radius: 8px;
padding: 11px 12px;
background: #ffffff;
color: var(--text);
}
.sidebar input {
border-color: rgba(255,255,255,0.16);
background: rgba(255,255,255,0.08);
color: #ffffff;
}
dl {
display: grid;
gap: 9px;
margin: 0;
}
dl div {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
dt {
color: #9ca3af;
font-size: 12px;
}
dd {
margin: 0;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #e5edf7;
font-size: 12px;
}
[data-tone="ok"] { color: #86efac; }
[data-tone="warn"] { color: #fde68a; }
[data-tone="bad"] { color: #fca5a5; }
.chat-shell {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto auto;
gap: 16px;
min-width: 0;
height: 100vh;
padding: 22px;
}
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 18px 20px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow);
}
.chat-header h2 {
font-size: 20px;
}
.chat-header p {
margin-top: 4px;
color: var(--muted);
font-size: 13px;
}
.secondary-button,
.composer button {
border: 0;
border-radius: 8px;
padding: 10px 14px;
font-weight: 750;
}
.secondary-button {
background: #edf2f7;
color: #1f2937;
}
.messages {
display: flex;
flex-direction: column;
gap: 14px;
min-height: 0;
overflow-y: auto;
padding: 18px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow);
}
.message {
display: grid;
grid-template-columns: 36px minmax(0, 1fr);
gap: 10px;
max-width: 860px;
}
.message.user {
align-self: flex-end;
grid-template-columns: minmax(0, 1fr) 36px;
}
.message.user .avatar {
grid-column: 2;
grid-row: 1;
background: #dbeafe;
color: #1d4ed8;
}
.message.assistant .avatar {
background: #e5e7eb;
color: #111827;
}
.message.user .bubble {
grid-column: 1;
grid-row: 1;
background: #eff6ff;
border-color: #bfdbfe;
}
.bubble {
padding: 12px 14px;
background: var(--panel-strong);
border: 1px solid var(--border);
border-radius: 8px;
}
.bubble p {
margin: 8px 0 0;
white-space: pre-wrap;
overflow-wrap: anywhere;
line-height: 1.5;
}
.message-reasoning {
display: grid;
gap: 8px;
margin-top: 10px;
padding: 9px 10px;
background: #f1f5f9;
border: 1px solid #dbe3ee;
border-radius: 8px;
}
.message-reasoning.is-collapsed {
gap: 0;
}
.message-reasoning-toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
padding: 0;
background: transparent;
color: #475569;
font-size: 12px;
font-weight: 750;
text-align: left;
}
.message-reasoning-status {
flex: 0 0 auto;
padding: 2px 7px;
border-radius: 999px;
background: #e2e8f0;
color: #64748b;
font-size: 11px;
}
.message-reasoning pre {
margin: 0;
max-height: 220px;
overflow: auto;
color: #334155;
font-size: 12px;
line-height: 1.45;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.tool-terminal {
margin-top: 10px;
overflow: hidden;
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 8px;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
}
.tool-terminal-header {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
min-height: 34px;
padding: 8px 10px;
background: #111827;
border-bottom: 1px solid #1e293b;
}
.terminal-dots {
display: flex;
gap: 5px;
}
.terminal-dots i {
width: 9px;
height: 9px;
border-radius: 999px;
}
.terminal-dots i:nth-child(1) { background: #ef4444; }
.terminal-dots i:nth-child(2) { background: #f59e0b; }
.terminal-dots i:nth-child(3) { background: #22c55e; }
.tool-terminal-title {
min-width: 0;
overflow: hidden;
color: #d1d5db;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.tool-terminal-status {
padding: 2px 7px;
border-radius: 999px;
background: #1d4ed8;
color: #dbeafe;
font-size: 11px;
font-weight: 800;
}
.tool-terminal.is-error .tool-terminal-status {
background: #7f1d1d;
color: #fecaca;
}
.tool-terminal.is-waiting .tool-terminal-status {
background: #854d0e;
color: #fef3c7;
}
.tool-terminal-body {
margin: 0;
max-height: 220px;
overflow: auto;
padding: 10px 12px;
color: #d1fae5;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
line-height: 1.55;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.message-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: var(--muted);
font-size: 12px;
}
.message-meta strong {
color: var(--text);
font-size: 13px;
}
.debug-panel {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 16px;
min-height: 180px;
}
.debug-column {
min-width: 0;
padding: 14px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
}
.debug-column h3 {
margin: 0 0 10px;
font-size: 13px;
}
pre,
#events {
margin: 0;
max-height: 170px;
overflow: auto;
color: #334155;
font-size: 12px;
line-height: 1.45;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
#events {
display: grid;
gap: 8px;
padding-left: 18px;
}
#events li strong,
#events li span {
display: block;
}
#events li span {
color: var(--muted);
}
.composer {
display: grid;
gap: 10px;
padding: 14px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow);
}
.composer textarea {
min-height: 86px;
resize: vertical;
}
.composer-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
#composer-hint {
color: var(--muted);
font-size: 12px;
}
.composer button {
min-width: 96px;
background: var(--accent);
color: #ffffff;
}
.composer button:hover {
background: var(--accent-strong);
}
.composer button:disabled {
cursor: wait;
opacity: 0.7;
}
[hidden] {
display: none !important;
}
@media (max-width: 860px) {
.app-shell {
grid-template-columns: 1fr;
}
.sidebar {
min-height: auto;
}
.chat-shell {
height: auto;
min-height: 100vh;
}
.chat-header,
.debug-panel,
.composer-actions {
grid-template-columns: 1fr;
flex-direction: column;
align-items: stretch;
}
.debug-panel {
display: grid;
}
}