fix(ui): landing cleanup (#475) (thanks @rahthakor)
This commit is contained in:
@@ -68,6 +68,7 @@
|
||||
- Sessions: support session `label` in store/list/UI and allow `sessions_send` lookup by label. (#570) — thanks @azade-c
|
||||
- Control UI: show/patch per-session reasoning level and render extracted reasoning in chat.
|
||||
- Control UI: queue outgoing chat messages, add Enter-to-send, and show queued items. (#527) — thanks @YuriNachos
|
||||
- Control UI: refactor chat layout with tool sidebar, grouped messages, and nav improvements. (#475) — thanks @rahthakor
|
||||
- Control UI: drop explicit `ui:install` step; `ui:build` now auto-installs UI deps (docs + update flow).
|
||||
- Telegram: retry long-polling conflicts with backoff to avoid fatal exits.
|
||||
- Telegram: fix grammY fetch type mismatch when injecting `fetch`. (#512) — thanks @YuriNachos
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { resetProcessRegistryForTests } from "./bash-process-registry.js";
|
||||
import {
|
||||
bashTool,
|
||||
@@ -50,6 +50,16 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe("bash tool backgrounding", () => {
|
||||
const originalShell = process.env.SHELL;
|
||||
|
||||
beforeEach(() => {
|
||||
if (!isWin) process.env.SHELL = "/bin/bash";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (!isWin) process.env.SHELL = originalShell;
|
||||
});
|
||||
|
||||
it(
|
||||
"backgrounds after yield and can be polled",
|
||||
async () => {
|
||||
@@ -171,9 +181,7 @@ describe("bash tool backgrounding", () => {
|
||||
expect(text).toContain("hi");
|
||||
});
|
||||
|
||||
// Skip: Fails when user's shell config (.zshenv) sources files that don't exist in test env,
|
||||
// adding extra lines to stdout and breaking line count assertions.
|
||||
it.skip("logs line-based slices and defaults to last lines", async () => {
|
||||
it("logs line-based slices and defaults to last lines", async () => {
|
||||
const result = await bashTool.execute("call1", {
|
||||
command: echoLines(["one", "two", "three"]),
|
||||
background: true,
|
||||
@@ -193,9 +201,7 @@ describe("bash tool backgrounding", () => {
|
||||
expect(status).toBe("completed");
|
||||
});
|
||||
|
||||
// Skip: Fails when user's shell config (.zshenv) sources files that don't exist in test env,
|
||||
// adding extra lines to stdout and breaking offset assertions.
|
||||
it.skip("supports line offsets for log slices", async () => {
|
||||
it("supports line offsets for log slices", async () => {
|
||||
const result = await bashTool.execute("call1", {
|
||||
command: echoLines(["alpha", "beta", "gamma"]),
|
||||
background: true,
|
||||
|
||||
@@ -6,13 +6,9 @@
|
||||
<title>Clawdbot Control</title>
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<!-- Flaticon Uicons - Free icon font -->
|
||||
<link rel="stylesheet" href="https://cdn-uicons.flaticon.com/2.6.0/uicons-regular-rounded/css/uicons-regular-rounded.css" />
|
||||
<link rel="stylesheet" href="https://cdn-uicons.flaticon.com/2.6.0/uicons-solid-rounded/css/uicons-solid-rounded.css" />
|
||||
</head>
|
||||
<body>
|
||||
<clawdbot-app></clawdbot-app>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
--shell-pad: 16px;
|
||||
--shell-gap: 16px;
|
||||
--shell-nav-width: 220px;
|
||||
--shell-nav-collapsed-width: 56px;
|
||||
--shell-topbar-height: 56px;
|
||||
--shell-focus-duration: 220ms;
|
||||
--shell-focus-ease: cubic-bezier(0.2, 0.85, 0.25, 1);
|
||||
@@ -19,7 +18,7 @@
|
||||
}
|
||||
|
||||
.shell--nav-collapsed {
|
||||
grid-template-columns: var(--shell-nav-collapsed-width) minmax(0, 1fr);
|
||||
grid-template-columns: 0px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.shell--chat-focus {
|
||||
@@ -298,13 +297,12 @@
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
min-height: 0;
|
||||
height: calc(100vh - var(--shell-pad) * 2 - var(--topbar-height, 80px) - var(--shell-gap));
|
||||
overflow-y: auto; /* Enable vertical scrolling for pages with long content */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.shell--chat .content {
|
||||
height: calc(100vh - var(--shell-pad) * 2 - var(--topbar-height, 80px) - var(--shell-gap));
|
||||
/* No-op: keep chat layout consistent with other tabs */
|
||||
}
|
||||
|
||||
.docs-link {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { html, nothing } from "lit";
|
||||
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
|
||||
import {
|
||||
TAB_GROUPS,
|
||||
iconClassForTab,
|
||||
iconForTab,
|
||||
pathForTab,
|
||||
subtitleForTab,
|
||||
titleForTab,
|
||||
@@ -608,7 +608,7 @@ function renderTab(state: AppViewState, tab: Tab) {
|
||||
}}
|
||||
title=${titleForTab(tab)}
|
||||
>
|
||||
<i class="nav-item__icon ${iconClassForTab(tab)}"></i>
|
||||
<span class="nav-item__icon" aria-hidden="true">${iconForTab(tab)}</span>
|
||||
<span class="nav-item__text">${titleForTab(tab)}</span>
|
||||
</a>
|
||||
`;
|
||||
@@ -620,8 +620,9 @@ function renderChatControls(state: AppViewState) {
|
||||
const listIcon = html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>`;
|
||||
// Icon for grouped view
|
||||
const groupIcon = html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>`;
|
||||
// Refresh icon (Flaticon style)
|
||||
// Refresh icon
|
||||
const refreshIcon = html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path></svg>`;
|
||||
const focusIcon = html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h3"></path><path d="M20 7V4h-3"></path><path d="M4 17v3h3"></path><path d="M20 17v3h-3"></path><circle cx="12" cy="12" r="3"></circle></svg>`;
|
||||
return html`
|
||||
<div class="chat-controls">
|
||||
<label class="field chat-controls__session">
|
||||
@@ -637,7 +638,11 @@ function renderChatControls(state: AppViewState) {
|
||||
state.chatRunId = null;
|
||||
state.resetToolStream();
|
||||
state.resetChatScroll();
|
||||
state.applySettings({ ...state.settings, sessionKey: next });
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
sessionKey: next,
|
||||
lastActiveSessionKey: next,
|
||||
});
|
||||
void loadChatHistory(state);
|
||||
}}
|
||||
>
|
||||
@@ -661,6 +666,18 @@ function renderChatControls(state: AppViewState) {
|
||||
${refreshIcon}
|
||||
</button>
|
||||
<span class="chat-controls__separator">|</span>
|
||||
<button
|
||||
class="btn btn--sm btn--icon ${state.settings.chatFocusMode ? "active" : ""}"
|
||||
@click=${() =>
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
chatFocusMode: !state.settings.chatFocusMode,
|
||||
})}
|
||||
aria-pressed=${state.settings.chatFocusMode}
|
||||
title="Toggle focus mode (hide sidebar + page header)"
|
||||
>
|
||||
${focusIcon}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm btn--icon ${state.settings.useNewChatLayout ? "active" : ""}"
|
||||
@click=${() =>
|
||||
|
||||
@@ -205,6 +205,7 @@ export class ClawdbotApp extends LitElement {
|
||||
@state() eventLog: EventLogEntry[] = [];
|
||||
private eventLogBuffer: EventLogEntry[] = [];
|
||||
private toolStreamSyncTimer: number | null = null;
|
||||
private sidebarCloseTimer: number | null = null;
|
||||
|
||||
@state() sessionKey = this.settings.sessionKey;
|
||||
@state() chatLoading = false;
|
||||
@@ -1156,6 +1157,10 @@ export class ClawdbotApp extends LitElement {
|
||||
|
||||
// Sidebar handlers for tool output viewing
|
||||
handleOpenSidebar(content: string) {
|
||||
if (this.sidebarCloseTimer != null) {
|
||||
window.clearTimeout(this.sidebarCloseTimer);
|
||||
this.sidebarCloseTimer = null;
|
||||
}
|
||||
this.sidebarContent = content;
|
||||
this.sidebarError = null;
|
||||
this.sidebarOpen = true;
|
||||
@@ -1164,9 +1169,14 @@ export class ClawdbotApp extends LitElement {
|
||||
handleCloseSidebar() {
|
||||
this.sidebarOpen = false;
|
||||
// Clear content after transition
|
||||
setTimeout(() => {
|
||||
if (this.sidebarCloseTimer != null) {
|
||||
window.clearTimeout(this.sidebarCloseTimer);
|
||||
}
|
||||
this.sidebarCloseTimer = window.setTimeout(() => {
|
||||
if (this.sidebarOpen) return;
|
||||
this.sidebarContent = null;
|
||||
this.sidebarError = null;
|
||||
this.sidebarCloseTimer = null;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,21 +16,24 @@ beforeEach(() => {
|
||||
// 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 markdown rendering", () => {
|
||||
// Skip: Tool card rendering was refactored to use sidebar-based output display.
|
||||
// The .chat-tool-card__output class is only in the legacy renderer and requires
|
||||
// the <details> element to be expanded. New layout uses renderToolCard() which
|
||||
// shows preview/inline text without the __output wrapper.
|
||||
it.skip("renders markdown inside tool result cards", async () => {
|
||||
it("renders markdown inside tool result cards", async () => {
|
||||
localStorage.setItem(
|
||||
"clawdbot.control.settings.v1",
|
||||
JSON.stringify({ useNewChatLayout: false }),
|
||||
);
|
||||
|
||||
const app = mountApp("/chat");
|
||||
await app.updateComplete;
|
||||
|
||||
|
||||
@@ -28,10 +28,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("chat focus mode", () => {
|
||||
// Skip: Focus mode toggle button was moved to settings panel, no longer in chat view.
|
||||
// The shell--chat-focus class still works when settings.chatFocusMode is true,
|
||||
// but there's no in-chat toggle button to test.
|
||||
it.skip("collapses header + sidebar on chat tab only", async () => {
|
||||
it("collapses header + sidebar on chat tab only", async () => {
|
||||
const app = mountApp("/chat");
|
||||
await app.updateComplete;
|
||||
|
||||
@@ -68,4 +65,3 @@ describe("chat focus mode", () => {
|
||||
expect(shell?.classList.contains("shell--chat-focus")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
TAB_GROUPS,
|
||||
iconClassForTab,
|
||||
iconForTab,
|
||||
inferBasePathFromPathname,
|
||||
normalizeBasePath,
|
||||
normalizePath,
|
||||
@@ -16,33 +16,34 @@ import {
|
||||
/** All valid tab identifiers derived from TAB_GROUPS */
|
||||
const ALL_TABS: Tab[] = TAB_GROUPS.flatMap((group) => group.tabs) as Tab[];
|
||||
|
||||
describe("iconClassForTab", () => {
|
||||
describe("iconForTab", () => {
|
||||
it("returns a non-empty string for every tab", () => {
|
||||
for (const tab of ALL_TABS) {
|
||||
const icon = iconClassForTab(tab);
|
||||
const icon = iconForTab(tab);
|
||||
expect(icon).toBeTruthy();
|
||||
expect(typeof icon).toBe("string");
|
||||
expect(icon.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns expected icon classes for each tab", () => {
|
||||
expect(iconClassForTab("chat")).toBe("fi fi-rr-comment");
|
||||
expect(iconClassForTab("overview")).toBe("fi fi-rr-chart-histogram");
|
||||
expect(iconClassForTab("connections")).toBe("fi fi-rr-link");
|
||||
expect(iconClassForTab("instances")).toBe("fi fi-rr-radar");
|
||||
expect(iconClassForTab("sessions")).toBe("fi fi-rr-document");
|
||||
expect(iconClassForTab("cron")).toBe("fi fi-rr-clock");
|
||||
expect(iconClassForTab("skills")).toBe("fi fi-rr-bolt");
|
||||
expect(iconClassForTab("nodes")).toBe("fi fi-rr-computer");
|
||||
expect(iconClassForTab("config")).toBe("fi fi-rr-settings");
|
||||
expect(iconClassForTab("debug")).toBe("fi fi-rr-bug");
|
||||
it("returns stable icons for known tabs", () => {
|
||||
expect(iconForTab("chat")).toBe("💬");
|
||||
expect(iconForTab("overview")).toBe("📊");
|
||||
expect(iconForTab("connections")).toBe("🔗");
|
||||
expect(iconForTab("instances")).toBe("📡");
|
||||
expect(iconForTab("sessions")).toBe("📄");
|
||||
expect(iconForTab("cron")).toBe("⏰");
|
||||
expect(iconForTab("skills")).toBe("⚡️");
|
||||
expect(iconForTab("nodes")).toBe("🖥️");
|
||||
expect(iconForTab("config")).toBe("⚙️");
|
||||
expect(iconForTab("debug")).toBe("🐞");
|
||||
expect(iconForTab("logs")).toBe("🧾");
|
||||
});
|
||||
|
||||
it("returns fallback icon class for unknown tab", () => {
|
||||
it("returns a fallback icon for unknown tab", () => {
|
||||
// TypeScript won't allow this normally, but runtime could receive unexpected values
|
||||
const unknownTab = "unknown" as Tab;
|
||||
expect(iconClassForTab(unknownTab)).toBe("fi fi-rr-file");
|
||||
expect(iconForTab(unknownTab)).toBe("📁");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -98,42 +98,35 @@ export function inferBasePathFromPathname(pathname: string): string {
|
||||
return `/${segments.join("/")}`;
|
||||
}
|
||||
|
||||
/** Returns the Flaticon uicons class for a tab icon */
|
||||
export function iconClassForTab(tab: Tab): string {
|
||||
export function iconForTab(tab: Tab): string {
|
||||
switch (tab) {
|
||||
case "chat":
|
||||
return "fi fi-rr-comment"; // chat bubble
|
||||
return "💬";
|
||||
case "overview":
|
||||
return "fi fi-rr-chart-histogram"; // bar chart
|
||||
return "📊";
|
||||
case "connections":
|
||||
return "fi fi-rr-link"; // link
|
||||
return "🔗";
|
||||
case "instances":
|
||||
return "fi fi-rr-radar"; // radar/satellite
|
||||
return "📡";
|
||||
case "sessions":
|
||||
return "fi fi-rr-document"; // document
|
||||
return "📄";
|
||||
case "cron":
|
||||
return "fi fi-rr-clock"; // clock
|
||||
return "⏰";
|
||||
case "skills":
|
||||
return "fi fi-rr-bolt"; // lightning bolt
|
||||
return "⚡️";
|
||||
case "nodes":
|
||||
return "fi fi-rr-computer"; // computer
|
||||
return "🖥️";
|
||||
case "config":
|
||||
return "fi fi-rr-settings"; // gear
|
||||
return "⚙️";
|
||||
case "debug":
|
||||
return "fi fi-rr-bug"; // bug icon
|
||||
return "🐞";
|
||||
case "logs":
|
||||
return "fi fi-rr-file-code"; // file with code
|
||||
return "🧾";
|
||||
default:
|
||||
return "fi fi-rr-file"; // generic file
|
||||
return "📁";
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use iconClassForTab for better icon styling */
|
||||
export function iconForTab(tab: Tab): string {
|
||||
// Keep backward compatibility - return empty string, icons now use CSS classes
|
||||
return "";
|
||||
}
|
||||
|
||||
export function titleForTab(tab: Tab) {
|
||||
switch (tab) {
|
||||
case "overview":
|
||||
|
||||
Reference in New Issue
Block a user