feat: resize Skill Workshop proposal list

This commit is contained in:
Shakker
2026-05-31 16:53:50 +01:00
committed by Shakker
parent 299a023bd1
commit c74bb4475a
5 changed files with 139 additions and 5 deletions

View File

@@ -75,7 +75,7 @@
/* ── Triage two-pane card ───────────────────────────────────────────── */
.sw-triage {
display: grid;
grid-template-columns: 360px 1fr;
grid-template-columns: minmax(260px, var(--sw-queue-width, 360px)) 8px minmax(0, 1fr);
gap: 0;
flex: 1;
min-height: 0;
@@ -89,7 +89,6 @@
/* ── Queue (left) ───────────────────────────────────────────────────── */
.sw-queue {
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
min-width: 0;
@@ -97,6 +96,20 @@
align-self: stretch;
}
.sw-queue-resizer {
border-left: 1px solid var(--border);
border-right: 1px solid var(--border);
cursor: col-resize;
min-width: 8px;
touch-action: none;
}
.sw-queue-resizer:hover,
.sw-queue-resizer:focus-visible {
background: color-mix(in srgb, var(--accent) 16%, transparent);
outline: none;
}
.sw-queue__head {
padding: 14px 16px;
border-bottom: 1px solid var(--border);
@@ -525,3 +538,13 @@
color: var(--text-strong);
text-decoration-color: var(--accent);
}
@media (max-width: 760px) {
.sw-triage {
grid-template-columns: minmax(0, 1fr);
}
.sw-queue-resizer {
display: none;
}
}

View File

@@ -487,7 +487,11 @@ let clawhubSearchTimer: ReturnType<typeof setTimeout> | null = null;
const UPDATE_BANNER_DISMISS_KEY = "openclaw:control-ui:update-banner-dismissed:v1";
const SKILL_WORKSHOP_REVIEWED_KEY = "openclaw:control-ui:skill-workshop-reviewed:v1";
const SKILL_WORKSHOP_QUEUE_WIDTH_KEY = "openclaw:control-ui:skill-workshop-queue-width:v1";
const MAX_SKILL_WORKSHOP_REVIEWED_KEYS = 500;
const DEFAULT_SKILL_WORKSHOP_QUEUE_WIDTH = 360;
const MIN_SKILL_WORKSHOP_QUEUE_WIDTH = 280;
const MAX_SKILL_WORKSHOP_QUEUE_WIDTH = 560;
const CRON_THINKING_SUGGESTIONS = ["off", "minimal", "low", "medium", "high"];
const CRON_TIMEZONE_SUGGESTIONS = [
"UTC",
@@ -570,6 +574,40 @@ function saveSkillWorkshopReviewedKeys(keys: string[]): void {
}
}
export function loadSkillWorkshopQueueWidth(): number {
const raw = getSafeLocalStorage()?.getItem(SKILL_WORKSHOP_QUEUE_WIDTH_KEY);
if (!raw) {
return DEFAULT_SKILL_WORKSHOP_QUEUE_WIDTH;
}
return clampSkillWorkshopQueueWidth(Number(raw));
}
function clampSkillWorkshopQueueWidth(width: number): number {
if (!Number.isFinite(width)) {
return DEFAULT_SKILL_WORKSHOP_QUEUE_WIDTH;
}
return Math.min(
MAX_SKILL_WORKSHOP_QUEUE_WIDTH,
Math.max(MIN_SKILL_WORKSHOP_QUEUE_WIDTH, Math.round(width)),
);
}
function setSkillWorkshopQueueWidth(
state: AppViewState,
width: number,
options?: { persist?: boolean },
): void {
const next = clampSkillWorkshopQueueWidth(width);
state.skillWorkshopQueueWidth = next;
if (options?.persist) {
try {
getSafeLocalStorage()?.setItem(SKILL_WORKSHOP_QUEUE_WIDTH_KEY, String(next));
} catch {
// Width persistence is a convenience; the current drag state still applies.
}
}
}
function skillWorkshopReviewKey(proposal: SkillWorkshopReviewableProposal): string {
return `${proposal.key}:${proposal.version}:${proposal.updatedAt ?? proposal.createdAt}`;
}
@@ -2956,10 +2994,14 @@ export function renderApp(state: AppViewState) {
query: state.skillWorkshopQuery,
filePreviewKey: state.skillWorkshopFilePreviewKey,
filePreviewQuery: state.skillWorkshopFilePreviewQuery,
queueWidth: state.skillWorkshopQueueWidth,
counts,
onStatusFilterChange: (next) => (state.skillWorkshopStatusFilter = next),
onQueryChange: (next) => (state.skillWorkshopQuery = next),
onFilePreviewQueryChange: (next) => (state.skillWorkshopFilePreviewQuery = next),
onQueueWidthChange: (width) => setSkillWorkshopQueueWidth(state, width),
onQueueWidthCommit: (width) =>
setSkillWorkshopQueueWidth(state, width, { persist: true }),
onSelect: (key) => {
const proposal = proposals.find((p) => p.key === key);
state.skillWorkshopSelectedKey = key;

View File

@@ -431,6 +431,7 @@ export type AppViewState = {
skillWorkshopFilePreviewKey: string | null;
skillWorkshopFilePreviewQuery: string;
skillWorkshopReviewedKeys: string[];
skillWorkshopQueueWidth: number;
healthLoading: boolean;
healthResult: HealthSummary | null;
healthError: string | null;

View File

@@ -42,7 +42,11 @@ import {
} from "./app-lifecycle.ts";
import { initNativeBridge } from "./app-native-bridge.ts";
import { createChatSession as createChatSessionInternal } from "./app-render.helpers.ts";
import { loadSkillWorkshopReviewedKeys, renderApp } from "./app-render.ts";
import {
loadSkillWorkshopQueueWidth,
loadSkillWorkshopReviewedKeys,
renderApp,
} from "./app-render.ts";
import {
exportLogs as exportLogsInternal,
handleActivityScroll as handleActivityScrollInternal,
@@ -634,6 +638,7 @@ export class OpenClawApp extends LitElement {
@state() skillWorkshopFilePreviewKey: string | null = null;
@state() skillWorkshopFilePreviewQuery = "";
@state() skillWorkshopReviewedKeys = loadSkillWorkshopReviewedKeys();
@state() skillWorkshopQueueWidth = loadSkillWorkshopQueueWidth();
@state() healthLoading = false;
@state() healthResult: HealthSummary | null = null;

View File

@@ -1,4 +1,5 @@
import { html, nothing } from "lit";
import { styleMap } from "lit/directives/style-map.js";
import "../components/file-preview-modal.ts";
export type SkillWorkshopProposalStatus =
@@ -40,10 +41,13 @@ export type SkillWorkshopProps = {
query: string;
filePreviewKey: string | null;
filePreviewQuery: string;
queueWidth: number;
counts: Record<SkillWorkshopStatusFilter, number>;
onStatusFilterChange: (status: SkillWorkshopStatusFilter) => void;
onQueryChange: (query: string) => void;
onFilePreviewQueryChange: (query: string) => void;
onQueueWidthChange: (width: number) => void;
onQueueWidthCommit: (width: number) => void;
onSelect: (key: string) => void;
onPrev: () => void;
onNext: () => void;
@@ -90,8 +94,8 @@ export function renderSkillWorkshop(props: SkillWorkshopProps) {
return html`
<section class="skill-workshop">
${renderLifecycleTabs(props)}
<div class="sw-triage">
${renderQueue(props, groups, selected)}
<div class="sw-triage" style=${styleMap({ "--sw-queue-width": `${props.queueWidth}px` })}>
${renderQueue(props, groups, selected)} ${renderQueueResizer(props)}
${selected ? renderDetail(props, selected) : renderEmpty()}
</div>
</section>
@@ -113,6 +117,65 @@ export function renderSkillWorkshop(props: SkillWorkshopProps) {
`;
}
function renderQueueResizer(props: SkillWorkshopProps) {
return html`
<div
class="sw-queue-resizer"
role="separator"
aria-label="Resize proposal list"
aria-orientation="vertical"
tabindex="0"
@pointerdown=${(event: PointerEvent) => startQueueResize(event, props)}
@keydown=${(event: KeyboardEvent) => resizeQueueWithKeyboard(event, props)}
></div>
`;
}
function startQueueResize(event: PointerEvent, props: SkillWorkshopProps): void {
event.preventDefault();
event.stopPropagation();
const startX = event.clientX;
const startWidth = props.queueWidth;
let currentWidth = startWidth;
const body = document.body;
const previousCursor = body.style.cursor;
const previousUserSelect = body.style.userSelect;
body.style.cursor = "col-resize";
body.style.userSelect = "none";
const cleanup = () => {
window.removeEventListener("pointermove", onMove);
window.removeEventListener("pointerup", onUp);
window.removeEventListener("pointercancel", onUp);
body.style.cursor = previousCursor;
body.style.userSelect = previousUserSelect;
};
const onMove = (moveEvent: PointerEvent) => {
currentWidth = startWidth + moveEvent.clientX - startX;
props.onQueueWidthChange(currentWidth);
};
const onUp = () => {
cleanup();
props.onQueueWidthCommit(currentWidth);
};
window.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", onUp);
window.addEventListener("pointercancel", onUp);
}
function resizeQueueWithKeyboard(event: KeyboardEvent, props: SkillWorkshopProps): void {
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") {
return;
}
event.preventDefault();
const delta = event.key === "ArrowLeft" ? -24 : 24;
props.onQueueWidthCommit(props.queueWidth + delta);
}
function renderLifecycleTabs(props: SkillWorkshopProps) {
return html`
<div class="sw-lifecycle-tabs">