diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a4389b6e67..d080cc92692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Changes - Control UI/header: show the active agent name in dashboard breadcrumbs without adding the current session key, keeping non-chat views oriented without crowding the topbar. +- Control UI/cron: make the New Job sidebar collapsible so the jobs list can reclaim space while keeping the form one click away. Thanks @BunsDev. - Gateway/startup: keep model-catalog test helpers, run-session lookup code, QR pairing helpers, and TypeBox memory-tool schema construction out of hot startup import paths, reducing default gateway benchmark plugin-load and memory pressure. - Control UI/performance: record browser long animation frame or long task entries in the debug event log when supported, making slow dashboard renders easier to attribute from the UI. - Channels/streaming: add unified `streaming.mode: "progress"` drafts with auto single-word status labels and shared progress configuration across Discord, Telegram, Matrix, Slack, and Microsoft Teams. diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index 56cff23b021..b7f6e4ea1b1 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -1008,6 +1008,10 @@ align-items: start; } +.cron-workspace--form-collapsed { + grid-template-columns: minmax(0, 1fr) 64px; +} + .cron-workspace-main { display: grid; gap: 16px; @@ -1021,12 +1025,62 @@ overflow-y: auto; } +.cron-workspace-form--collapsed { + min-height: 180px; + overflow: hidden; + padding: 10px; +} + +.cron-form-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.cron-form-header__copy { + min-width: 0; +} + +.cron-form-collapse-toggle { + flex: 0 0 auto; + width: 34px; + height: 34px; + padding: 0; + justify-content: center; +} + +.cron-workspace-form--collapsed .cron-form-header { + min-height: 160px; + flex-direction: column; + align-items: center; + justify-content: space-between; +} + +.cron-workspace-form--collapsed .cron-form-header__copy { + display: flex; + justify-content: center; + writing-mode: vertical-rl; + transform: rotate(180deg); + white-space: nowrap; +} + +.cron-workspace-form--collapsed .card-title { + font-size: 13px; +} + .cron-form { margin-top: 16px; display: grid; gap: 14px; } +.cron-form[hidden], +.cron-form-status[hidden], +.cron-form-actions[hidden] { + display: none; +} + .cron-form-section { border: 1px solid var(--border); border-radius: var(--radius-md); @@ -1399,6 +1453,25 @@ order: -1; } + .cron-workspace--form-collapsed { + grid-template-columns: 1fr; + } + + .cron-workspace-form--collapsed { + min-height: 0; + } + + .cron-workspace-form--collapsed .cron-form-header { + min-height: 0; + flex-direction: row; + align-items: center; + } + + .cron-workspace-form--collapsed .cron-form-header__copy { + writing-mode: horizontal-tb; + transform: none; + } + .cron-form-grid { grid-template-columns: 1fr; gap: 12px; diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 1e37f2b6ecd..30d3e071e04 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -1814,6 +1814,7 @@ export function renderApp(state: AppViewState) { error: state.cronError, busy: state.cronBusy, form: state.cronForm, + cronFormCollapsed: state.cronFormCollapsed, channels: state.channelsSnapshot?.channelMeta?.length ? state.channelsSnapshot.channelMeta.map((entry) => entry.id) : (state.channelsSnapshot?.channelOrder ?? []), @@ -1844,9 +1845,18 @@ export function renderApp(state: AppViewState) { }, onRefresh: () => state.loadCron(), onAdd: () => addCronJob(state), - onEdit: (job) => startCronEdit(state, job), - onClone: (job) => startCronClone(state, job), + onEdit: (job) => { + state.cronFormCollapsed = false; + startCronEdit(state, job); + }, + onClone: (job) => { + state.cronFormCollapsed = false; + startCronClone(state, job); + }, onCancelEdit: () => cancelCronEdit(state), + onToggleFormCollapsed: (collapsed) => { + state.cronFormCollapsed = collapsed; + }, onToggle: (job, enabled) => toggleCronJob(state, job, enabled), onRun: (job, mode) => runCronJob(state, job, mode ?? "force"), onRemove: (job) => removeCronJob(state, job), diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index f728a62b21f..0203ea03e5d 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -330,6 +330,7 @@ export type AppViewState = { | "cronStatus" | "cronError" | "cronForm" + | "cronFormCollapsed" | "cronFieldErrors" | "cronEditingJobId" | "cronRunsJobId" diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 5d263701aa9..fb668b6da80 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -471,6 +471,7 @@ export class OpenClawApp extends LitElement { @state() cronStatus: CronStatus | null = null; @state() cronError: string | null = null; @state() cronForm: CronFormState = { ...DEFAULT_CRON_FORM }; + @state() cronFormCollapsed = false; @state() cronFieldErrors: import("./controllers/cron.js").CronFieldErrors = {}; @state() cronEditingJobId: string | null = null; @state() cronRunsJobId: string | null = null; diff --git a/ui/src/ui/controllers/cron.test.ts b/ui/src/ui/controllers/cron.test.ts index 56f47e39fc9..3c8814caaf2 100644 --- a/ui/src/ui/controllers/cron.test.ts +++ b/ui/src/ui/controllers/cron.test.ts @@ -38,6 +38,7 @@ function createState(overrides: Partial = {}): CronState { cronStatus: null, cronError: null, cronForm: { ...DEFAULT_CRON_FORM }, + cronFormCollapsed: false, cronFieldErrors: {}, cronEditingJobId: null, cronRunsJobId: null, diff --git a/ui/src/ui/controllers/cron.ts b/ui/src/ui/controllers/cron.ts index 141b22d6801..e1aafd866f6 100644 --- a/ui/src/ui/controllers/cron.ts +++ b/ui/src/ui/controllers/cron.ts @@ -67,6 +67,7 @@ export type CronState = { cronStatus: CronStatus | null; cronError: string | null; cronForm: CronFormState; + cronFormCollapsed: boolean; cronFieldErrors: CronFieldErrors; cronEditingJobId: string | null; cronRunsJobId: string | null; diff --git a/ui/src/ui/views/cron.test.ts b/ui/src/ui/views/cron.test.ts index 51aff0ee793..e1fdf776e16 100644 --- a/ui/src/ui/views/cron.test.ts +++ b/ui/src/ui/views/cron.test.ts @@ -277,6 +277,45 @@ describe("cron view", () => { expect(container.querySelector('input[placeholder="https://example.com/cron"]')).toBeNull(); }); + it("collapses the new job sidebar without rendering the full form", () => { + const container = document.createElement("div"); + const onToggleFormCollapsed = vi.fn(); + const expandedProps = createProps() as CronProps & { + cronFormCollapsed: boolean; + onToggleFormCollapsed: (collapsed: boolean) => void; + }; + expandedProps.cronFormCollapsed = false; + expandedProps.onToggleFormCollapsed = onToggleFormCollapsed; + + render(renderCron(expandedProps), container); + + const collapseButton = container.querySelector('[data-test-id="cron-form-collapse-toggle"]'); + expect(collapseButton).not.toBeNull(); + expect(collapseButton?.getAttribute("aria-expanded")).toBe("true"); + collapseButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + expect(onToggleFormCollapsed).toHaveBeenCalledWith(true); + expect(container.querySelector(".cron-form")).not.toBeNull(); + + const collapsedProps = createProps() as CronProps & { + cronFormCollapsed: boolean; + onToggleFormCollapsed: (collapsed: boolean) => void; + }; + collapsedProps.cronFormCollapsed = true; + collapsedProps.onToggleFormCollapsed = onToggleFormCollapsed; + + render(renderCron(collapsedProps), container); + + const collapsedButton = container.querySelector('[data-test-id="cron-form-collapse-toggle"]'); + expect(container.querySelector(".cron-workspace--form-collapsed")).not.toBeNull(); + expect(container.querySelector(".cron-workspace-form--collapsed")).not.toBeNull(); + expect(collapsedButton?.getAttribute("aria-expanded")).toBe("false"); + expect(container.querySelector(".cron-form")?.hasAttribute("hidden")).toBe(true); + expect(container.querySelector(".cron-form-actions")?.hasAttribute("hidden")).toBe(true); + + collapsedButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + expect(onToggleFormCollapsed).toHaveBeenLastCalledWith(false); + }); + it("shows webhook delivery details for jobs", () => { const container = document.createElement("div"); const job = { diff --git a/ui/src/ui/views/cron.ts b/ui/src/ui/views/cron.ts index 73e932c9e6c..d37f7055cd9 100644 --- a/ui/src/ui/views/cron.ts +++ b/ui/src/ui/views/cron.ts @@ -45,6 +45,7 @@ export type CronProps = { fieldErrors: CronFieldErrors; canSubmit: boolean; editingJobId: string | null; + cronFormCollapsed?: boolean; channels: string[]; channelLabels?: Record; channelMeta?: ChannelUiMetaEntry[]; @@ -71,6 +72,7 @@ export type CronProps = { onEdit: (job: CronJob) => void; onClone: (job: CronJob) => void; onCancelEdit: () => void; + onToggleFormCollapsed?: (collapsed: boolean) => void; onToggle: (job: CronJob, enabled: boolean) => void; onRun: (job: CronJob, mode?: "force" | "due") => void; onRemove: (job: CronJob) => void; @@ -383,6 +385,9 @@ export function renderCron(props: CronProps) { props.form.sessionTarget !== "main" && props.form.payloadKind === "agentTurn"; const selectedDeliveryMode = props.form.deliveryMode === "announce" && !supportsAnnounce ? "none" : props.form.deliveryMode; + const formCollapsed = props.cronFormCollapsed === true; + const formTitle = isEditing ? t("cron.form.editJob") : t("cron.form.newJob"); + const toggleFormCollapsed = props.onToggleFormCollapsed; const blockingFields = collectBlockingFields(props.fieldErrors, props.form, selectedDeliveryMode); const blockedByValidation = !props.busy && blockingFields.length > 0; const hasActiveJobsFilters = @@ -437,7 +442,7 @@ export function renderCron(props: CronProps) { -
+
-
-
${isEditing ? t("cron.form.editJob") : t("cron.form.newJob")}
-
- ${isEditing ? t("cron.form.updateSubtitle") : t("cron.form.createSubtitle")} +
+
+
+
${formTitle}
+ ${formCollapsed + ? nothing + : html` +
+ ${isEditing ? t("cron.form.updateSubtitle") : t("cron.form.createSubtitle")} +
+ `} +
+ ${toggleFormCollapsed + ? html` + + ` + : nothing}
-
+
${t( "cron.form.required", @@ -1317,7 +1347,12 @@ export function renderCron(props: CronProps) {
${blockedByValidation ? html` -
+
${t("cron.form.cantAddYet")}
${t("cron.form.fillRequired")}
    @@ -1338,7 +1373,7 @@ export function renderCron(props: CronProps) {
` : nothing} -
+