feat(control-ui): add chat focus mode
This commit is contained in:
@@ -55,6 +55,7 @@
|
|||||||
- Control UI: stabilize chat streaming during tool runs (no flicker/vanishing text; correct run scoping).
|
- Control UI: stabilize chat streaming during tool runs (no flicker/vanishing text; correct run scoping).
|
||||||
- Control UI: let config-form enums select empty-string values. Thanks @sreekaransrinath for PR #268.
|
- Control UI: let config-form enums select empty-string values. Thanks @sreekaransrinath for PR #268.
|
||||||
- Control UI: scroll chat to bottom on initial load. Thanks @kiranjd for PR #274.
|
- Control UI: scroll chat to bottom on initial load. Thanks @kiranjd for PR #274.
|
||||||
|
- Control UI: add Chat focus mode toggle to collapse header + sidebar.
|
||||||
- Status: show runtime (docker/direct) and move shortcuts to `/help`.
|
- Status: show runtime (docker/direct) and move shortcuts to `/help`.
|
||||||
- Status: show model auth source (api-key/oauth).
|
- Status: show model auth source (api-key/oauth).
|
||||||
- Block streaming: avoid splitting Markdown fenced blocks and reopen fences when forced to split.
|
- Block streaming: avoid splitting Markdown fenced blocks and reopen fences when forced to split.
|
||||||
|
|||||||
@@ -200,6 +200,11 @@
|
|||||||
background: rgba(245, 159, 74, 0.2);
|
background: rgba(245, 159, 74, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn.active {
|
||||||
|
border-color: rgba(245, 159, 74, 0.55);
|
||||||
|
background: rgba(245, 159, 74, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
.btn.danger {
|
.btn.danger {
|
||||||
border-color: rgba(255, 107, 107, 0.45);
|
border-color: rgba(255, 107, 107, 0.45);
|
||||||
background: rgba(255, 107, 107, 0.18);
|
background: rgba(255, 107, 107, 0.18);
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
.shell {
|
.shell {
|
||||||
--shell-pad: 18px;
|
--shell-pad: 18px;
|
||||||
--shell-gap: 18px;
|
--shell-gap: 18px;
|
||||||
|
--shell-nav-col: minmax(220px, 280px);
|
||||||
|
--shell-topbar-row: auto;
|
||||||
|
--shell-focus-duration: 220ms;
|
||||||
|
--shell-focus-ease: cubic-bezier(0.2, 0.85, 0.25, 1);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
|
grid-template-columns: var(--shell-nav-col) minmax(0, 1fr);
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: var(--shell-topbar-row) 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"topbar topbar"
|
"topbar topbar"
|
||||||
"nav content";
|
"nav content";
|
||||||
gap: var(--shell-gap);
|
gap: var(--shell-gap);
|
||||||
padding: var(--shell-pad);
|
padding: var(--shell-pad);
|
||||||
animation: dashboard-enter 0.6s ease-out;
|
animation: dashboard-enter 0.6s ease-out;
|
||||||
|
transition: padding var(--shell-focus-duration) var(--shell-focus-ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell--chat-focus {
|
||||||
|
--shell-pad: 10px;
|
||||||
|
--shell-gap: 12px;
|
||||||
|
--shell-nav-col: 0px;
|
||||||
|
--shell-topbar-row: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
@@ -27,6 +39,23 @@
|
|||||||
background: linear-gradient(135deg, var(--chrome), rgba(255, 255, 255, 0.02));
|
background: linear-gradient(135deg, var(--chrome), rgba(255, 255, 255, 0.02));
|
||||||
backdrop-filter: blur(18px);
|
backdrop-filter: blur(18px);
|
||||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
|
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
|
||||||
|
overflow: hidden;
|
||||||
|
transform-origin: top center;
|
||||||
|
transition: opacity var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
|
transform var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
|
max-height var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
|
padding var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
|
border-width var(--shell-focus-duration) var(--shell-focus-ease);
|
||||||
|
max-height: max(0px, var(--topbar-height, 92px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell--chat-focus .topbar {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
max-height: 0px;
|
||||||
|
padding: 0;
|
||||||
|
border-width: 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
@@ -72,6 +101,23 @@
|
|||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.25);
|
||||||
backdrop-filter: blur(18px);
|
backdrop-filter: blur(18px);
|
||||||
|
transform-origin: left center;
|
||||||
|
transition: opacity var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
|
transform var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
|
max-width var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
|
padding var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
|
border-width var(--shell-focus-duration) var(--shell-focus-ease);
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell--chat-focus .nav {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-12px);
|
||||||
|
max-width: 0px;
|
||||||
|
padding: 0;
|
||||||
|
border-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-group {
|
.nav-group {
|
||||||
@@ -163,6 +209,21 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
transform-origin: top center;
|
||||||
|
transition: opacity var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
|
transform var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
|
max-height var(--shell-focus-duration) var(--shell-focus-ease),
|
||||||
|
padding var(--shell-focus-duration) var(--shell-focus-ease);
|
||||||
|
max-height: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell--chat-focus .content-header {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
max-height: 0px;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
@@ -229,6 +290,7 @@
|
|||||||
.shell {
|
.shell {
|
||||||
--shell-pad: 12px;
|
--shell-pad: 12px;
|
||||||
--shell-gap: 12px;
|
--shell-gap: 12px;
|
||||||
|
--shell-nav-col: 1fr;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: auto auto 1fr;
|
grid-template-rows: auto auto 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
|
|||||||
@@ -185,9 +185,10 @@ export function renderApp(state: AppViewState) {
|
|||||||
const cronNext = state.cronStatus?.nextWakeAtMs ?? null;
|
const cronNext = state.cronStatus?.nextWakeAtMs ?? null;
|
||||||
const chatDisabledReason = state.connected ? null : "Disconnected from gateway.";
|
const chatDisabledReason = state.connected ? null : "Disconnected from gateway.";
|
||||||
const isChat = state.tab === "chat";
|
const isChat = state.tab === "chat";
|
||||||
|
const chatFocus = isChat && state.settings.chatFocusMode;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="shell ${isChat ? "shell--chat" : ""}">
|
<div class="shell ${isChat ? "shell--chat" : ""} ${chatFocus ? "shell--chat-focus" : ""}">
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<div class="brand-title">Clawdbot Control</div>
|
<div class="brand-title">Clawdbot Control</div>
|
||||||
@@ -398,10 +399,16 @@ export function renderApp(state: AppViewState) {
|
|||||||
disabledReason: chatDisabledReason,
|
disabledReason: chatDisabledReason,
|
||||||
error: state.lastError,
|
error: state.lastError,
|
||||||
sessions: state.sessionsResult,
|
sessions: state.sessionsResult,
|
||||||
|
focusMode: state.settings.chatFocusMode,
|
||||||
onRefresh: () => {
|
onRefresh: () => {
|
||||||
state.resetToolStream();
|
state.resetToolStream();
|
||||||
return loadChatHistory(state);
|
return loadChatHistory(state);
|
||||||
},
|
},
|
||||||
|
onToggleFocusMode: () =>
|
||||||
|
state.applySettings({
|
||||||
|
...state.settings,
|
||||||
|
chatFocusMode: !state.settings.chatFocusMode,
|
||||||
|
}),
|
||||||
onDraftChange: (next) => (state.chatMessage = next),
|
onDraftChange: (next) => (state.chatMessage = next),
|
||||||
onSend: () => state.handleSendChat(),
|
onSend: () => state.handleSendChat(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -70,10 +70,19 @@ describe("config form renderer", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const select = container.querySelector("select") as HTMLSelectElement | null;
|
const select = container.querySelector("select") as HTMLSelectElement | null;
|
||||||
expect(select).not.toBeNull();
|
const selects = Array.from(container.querySelectorAll("select"));
|
||||||
if (!select) return;
|
const modeSelect = selects.find((el) =>
|
||||||
select.value = "1";
|
Array.from(el.options).some((opt) => opt.textContent?.trim() === "token"),
|
||||||
select.dispatchEvent(new Event("change", { bubbles: true }));
|
) as HTMLSelectElement | undefined;
|
||||||
|
expect(modeSelect).not.toBeUndefined();
|
||||||
|
if (!modeSelect) return;
|
||||||
|
const tokenOption = Array.from(modeSelect.options).find(
|
||||||
|
(opt) => opt.textContent?.trim() === "token",
|
||||||
|
);
|
||||||
|
expect(tokenOption).not.toBeUndefined();
|
||||||
|
if (!tokenOption) return;
|
||||||
|
modeSelect.value = tokenOption.value;
|
||||||
|
modeSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
expect(onPatch).toHaveBeenCalledWith(["mode"], "token");
|
expect(onPatch).toHaveBeenCalledWith(["mode"], "token");
|
||||||
|
|
||||||
const checkbox = container.querySelector(
|
const checkbox = container.querySelector(
|
||||||
@@ -133,11 +142,16 @@ describe("config form renderer", () => {
|
|||||||
|
|
||||||
const selects = Array.from(container.querySelectorAll("select"));
|
const selects = Array.from(container.querySelectorAll("select"));
|
||||||
const bindSelect = selects.find((el) =>
|
const bindSelect = selects.find((el) =>
|
||||||
Array.from(el.options).some((opt) => opt.value === "tailnet"),
|
Array.from(el.options).some((opt) => opt.textContent?.trim() === "tailnet"),
|
||||||
) as HTMLSelectElement | undefined;
|
) as HTMLSelectElement | undefined;
|
||||||
expect(bindSelect).not.toBeUndefined();
|
expect(bindSelect).not.toBeUndefined();
|
||||||
if (!bindSelect) return;
|
if (!bindSelect) return;
|
||||||
bindSelect.value = "tailnet";
|
const tailnetOption = Array.from(bindSelect.options).find(
|
||||||
|
(opt) => opt.textContent?.trim() === "tailnet",
|
||||||
|
);
|
||||||
|
expect(tailnetOption).not.toBeUndefined();
|
||||||
|
if (!tailnetOption) return;
|
||||||
|
bindSelect.value = tailnetOption.value;
|
||||||
bindSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
bindSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
expect(onPatch).toHaveBeenCalledWith(["bind"], "tailnet");
|
expect(onPatch).toHaveBeenCalledWith(["bind"], "tailnet");
|
||||||
});
|
});
|
||||||
@@ -181,7 +195,7 @@ describe("config form renderer", () => {
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
mixed: {
|
mixed: {
|
||||||
anyOf: [{ type: "string" }, { type: "number" }],
|
anyOf: [{ type: "string" }, { type: "object", properties: {} }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
68
ui/src/ui/focus-mode.browser.test.ts
Normal file
68
ui/src/ui/focus-mode.browser.test.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { ClawdbotApp } from "./app";
|
||||||
|
|
||||||
|
const originalConnect = ClawdbotApp.prototype.connect;
|
||||||
|
|
||||||
|
function mountApp(pathname: string) {
|
||||||
|
window.history.replaceState({}, "", pathname);
|
||||||
|
const app = document.createElement("clawdbot-app") as ClawdbotApp;
|
||||||
|
document.body.append(app);
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ClawdbotApp.prototype.connect = () => {
|
||||||
|
// no-op: avoid real gateway WS connections in browser tests
|
||||||
|
};
|
||||||
|
window.__CLAWDBOT_CONTROL_UI_BASE_PATH__ = undefined;
|
||||||
|
localStorage.clear();
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
ClawdbotApp.prototype.connect = originalConnect;
|
||||||
|
window.__CLAWDBOT_CONTROL_UI_BASE_PATH__ = undefined;
|
||||||
|
localStorage.clear();
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("chat focus mode", () => {
|
||||||
|
it("collapses header + sidebar on chat tab only", async () => {
|
||||||
|
const app = mountApp("/chat");
|
||||||
|
await app.updateComplete;
|
||||||
|
|
||||||
|
const shell = app.querySelector(".shell");
|
||||||
|
expect(shell).not.toBeNull();
|
||||||
|
expect(shell?.classList.contains("shell--chat-focus")).toBe(false);
|
||||||
|
|
||||||
|
const toggle = app.querySelector<HTMLButtonElement>(
|
||||||
|
'button[title^="Toggle focus mode"]',
|
||||||
|
);
|
||||||
|
expect(toggle).not.toBeNull();
|
||||||
|
toggle?.click();
|
||||||
|
|
||||||
|
await app.updateComplete;
|
||||||
|
expect(shell?.classList.contains("shell--chat-focus")).toBe(true);
|
||||||
|
|
||||||
|
const link = app.querySelector<HTMLAnchorElement>('a.nav-item[href="/connections"]');
|
||||||
|
expect(link).not.toBeNull();
|
||||||
|
link?.dispatchEvent(
|
||||||
|
new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.updateComplete;
|
||||||
|
expect(app.tab).toBe("connections");
|
||||||
|
expect(shell?.classList.contains("shell--chat-focus")).toBe(false);
|
||||||
|
|
||||||
|
const chatLink = app.querySelector<HTMLAnchorElement>('a.nav-item[href="/chat"]');
|
||||||
|
chatLink?.dispatchEvent(
|
||||||
|
new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.updateComplete;
|
||||||
|
expect(app.tab).toBe("chat");
|
||||||
|
expect(shell?.classList.contains("shell--chat-focus")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -22,12 +22,14 @@ beforeEach(() => {
|
|||||||
// no-op: avoid real gateway WS connections in browser tests
|
// no-op: avoid real gateway WS connections in browser tests
|
||||||
};
|
};
|
||||||
window.__CLAWDBOT_CONTROL_UI_BASE_PATH__ = undefined;
|
window.__CLAWDBOT_CONTROL_UI_BASE_PATH__ = undefined;
|
||||||
|
localStorage.clear();
|
||||||
document.body.innerHTML = "";
|
document.body.innerHTML = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
ClawdbotApp.prototype.connect = originalConnect;
|
ClawdbotApp.prototype.connect = originalConnect;
|
||||||
window.__CLAWDBOT_CONTROL_UI_BASE_PATH__ = undefined;
|
window.__CLAWDBOT_CONTROL_UI_BASE_PATH__ = undefined;
|
||||||
|
localStorage.clear();
|
||||||
document.body.innerHTML = "";
|
document.body.innerHTML = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,13 +104,19 @@ describe("control UI routing", () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
await app.updateComplete;
|
await app.updateComplete;
|
||||||
await nextFrame();
|
for (let i = 0; i < 6; i++) {
|
||||||
|
await nextFrame();
|
||||||
|
}
|
||||||
|
|
||||||
const container = app.querySelector(".chat-thread") as HTMLElement | null;
|
const container = app.querySelector(".chat-thread") as HTMLElement | null;
|
||||||
expect(container).not.toBeNull();
|
expect(container).not.toBeNull();
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
const maxScroll = container.scrollHeight - container.clientHeight;
|
const maxScroll = container.scrollHeight - container.clientHeight;
|
||||||
expect(maxScroll).toBeGreaterThan(0);
|
expect(maxScroll).toBeGreaterThan(0);
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
if (container.scrollTop === maxScroll) break;
|
||||||
|
await nextFrame();
|
||||||
|
}
|
||||||
expect(container.scrollTop).toBe(maxScroll);
|
expect(container.scrollTop).toBe(maxScroll);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type UiSettings = {
|
|||||||
token: string;
|
token: string;
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
theme: ThemeMode;
|
theme: ThemeMode;
|
||||||
|
chatFocusMode: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function loadSettings(): UiSettings {
|
export function loadSettings(): UiSettings {
|
||||||
@@ -20,6 +21,7 @@ export function loadSettings(): UiSettings {
|
|||||||
token: "",
|
token: "",
|
||||||
sessionKey: "main",
|
sessionKey: "main",
|
||||||
theme: "system",
|
theme: "system",
|
||||||
|
chatFocusMode: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -42,6 +44,10 @@ export function loadSettings(): UiSettings {
|
|||||||
parsed.theme === "system"
|
parsed.theme === "system"
|
||||||
? parsed.theme
|
? parsed.theme
|
||||||
: defaults.theme,
|
: defaults.theme,
|
||||||
|
chatFocusMode:
|
||||||
|
typeof parsed.chatFocusMode === "boolean"
|
||||||
|
? parsed.chatFocusMode
|
||||||
|
: defaults.chatFocusMode,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return defaults;
|
return defaults;
|
||||||
|
|||||||
@@ -22,13 +22,14 @@ export type ChatProps = {
|
|||||||
disabledReason: string | null;
|
disabledReason: string | null;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
sessions: SessionsListResult | null;
|
sessions: SessionsListResult | null;
|
||||||
|
focusMode: boolean;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
|
onToggleFocusMode: () => void;
|
||||||
onDraftChange: (next: string) => void;
|
onDraftChange: (next: string) => void;
|
||||||
onSend: () => void;
|
onSend: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function renderChat(props: ChatProps) {
|
export function renderChat(props: ChatProps) {
|
||||||
const canInteract = props.connected;
|
|
||||||
const canCompose = props.connected && !props.sending;
|
const canCompose = props.connected && !props.sending;
|
||||||
const sessionOptions = resolveSessionOptions(props.sessionKey, props.sessions);
|
const sessionOptions = resolveSessionOptions(props.sessionKey, props.sessions);
|
||||||
const composePlaceholder = props.connected
|
const composePlaceholder = props.connected
|
||||||
@@ -43,7 +44,7 @@ export function renderChat(props: ChatProps) {
|
|||||||
<span>Session Key</span>
|
<span>Session Key</span>
|
||||||
<select
|
<select
|
||||||
.value=${props.sessionKey}
|
.value=${props.sessionKey}
|
||||||
?disabled=${!canInteract}
|
?disabled=${!props.connected}
|
||||||
@change=${(e: Event) =>
|
@change=${(e: Event) =>
|
||||||
props.onSessionKeyChange((e.target as HTMLSelectElement).value)}
|
props.onSessionKeyChange((e.target as HTMLSelectElement).value)}
|
||||||
>
|
>
|
||||||
@@ -57,7 +58,7 @@ export function renderChat(props: ChatProps) {
|
|||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
class="btn"
|
class="btn"
|
||||||
?disabled=${props.loading || !canInteract}
|
?disabled=${props.loading || !props.connected}
|
||||||
@click=${props.onRefresh}
|
@click=${props.onRefresh}
|
||||||
>
|
>
|
||||||
${props.loading ? "Loading…" : "Refresh"}
|
${props.loading ? "Loading…" : "Refresh"}
|
||||||
@@ -65,6 +66,14 @@ export function renderChat(props: ChatProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="chat-header__right">
|
<div class="chat-header__right">
|
||||||
<div class="muted">Thinking: ${props.thinkingLevel ?? "inherit"}</div>
|
<div class="muted">Thinking: ${props.thinkingLevel ?? "inherit"}</div>
|
||||||
|
<button
|
||||||
|
class="btn ${props.focusMode ? "active" : ""}"
|
||||||
|
@click=${props.onToggleFocusMode}
|
||||||
|
aria-pressed=${props.focusMode}
|
||||||
|
title="Toggle focus mode (hide header + sidebar)"
|
||||||
|
>
|
||||||
|
Focus
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ describe("config view", () => {
|
|||||||
schema: {
|
schema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
mixed: { anyOf: [{ type: "string" }, { type: "number" }] },
|
mixed: {
|
||||||
|
anyOf: [{ type: "string" }, { type: "object", properties: {} }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
schemaLoading: false,
|
schemaLoading: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user