feat(ui): complete composer bench state sweep

This commit is contained in:
vyctorbrzezowski
2026-08-22 12:48:56 -03:00
parent 6d33008302
commit 2179a78e2f
17 changed files with 296 additions and 174 deletions
+15
View File
@@ -249,3 +249,18 @@
2026-08-22 09:48 · Added the reconnect queue handle, narrative state sweep, open 40-skill sheet and multiline inline-skill fixture · ui/src/pages/chat/components/chat-composer-queue.ts, ui/src/test-helpers/composer-bench.ts · COMPONENT sweep coverage | BENCH
2026-08-22 09:48 · Introduced one attached sub-surface token for queue, goal and all status bands so attached chrome reads behind the composer in dark and light themes · ui/src/styles/chat/layout.css, ui/src/styles/components.css · COMPONENT attached surface hierarchy | BENCH
2026-08-22 11:17 · Replaced the Plan hover cards competing hover resize and transform transition with a frozen 300ms close delay plus one 100ms fade-scale exit, then reduced its width, padding and body type by about 20% · ui/src/components/session-progress-card.ts, ui/src/styles/chat/progress-card.css · COMPONENT Plan hover card | BENCH
2026-08-22 12:42 · Left-aligned Running tasks and Plan as a matched two-pill row when they coexist while preserving the standalone alignment rules and a shared compact preview language · ui/src/pages/chat/components/chat-background-tasks-status.ts, ui/src/styles/chat/tool-cards.css, ui/src/test-helpers/composer-bench.css, ui/src/test-helpers/composer-bench.ts · COMPONENT composer status neighbors | BENCH
2026-08-22 12:42 · Split Model and Effort into independent footer controls with their own chevrons, hover grounds and menus, separated by a 4px gap and no decorative dot · ui/src/pages/chat/components/chat-model-controls.ts, ui/src/pages/chat/components/chat-model-picker.ts, ui/src/pages/chat/components/chat-effort-picker.ts, ui/src/styles/chat/layout.css · COMPONENT composer footer controls | BENCH
2026-08-22 12:42 · Restored item-owned info and offline underlaps with low-opacity tinted surfaces, light semantic borders, shield and Globe Off icons · ui/src/components/icons.ts, ui/src/pages/chat/components/chat-composer-view.ts, ui/src/styles/chat/layout.css · COMPONENT composer status ownership | BENCH
2026-08-22 12:42 · Replaced the explicit newline in the inline-skill demo with natural desktop and mobile wrapping fixtures that preserve the atomic skill token · ui/src/test-helpers/composer-bench.ts · COMPONENT T3 invocation menus | BENCH
2026-08-22 12:42 · Removed Interrupted and approval cards from the composer sweep while preserving their product owners outside the demo sequence · ui/src/test-helpers/composer-bench.ts · COMPONENT composer sweep ownership | BENCH
2026-08-22 12:42 · Restored the established full-width dictation waveform and changed only the composer-edge orbit to a continuous 5s linear circuit · ui/src/styles/chat/layout.css · COMPONENT voice dictation | BENCH
2026-08-22 12:42 · Neutralized Plan progress in active and completed states while leaving success color exclusively on completed checks · ui/src/styles/chat/progress-card.css · COMPONENT Plan progress | BENCH
2026-08-22 12:42 · Sized scrollable queues to three full rows plus 40 percent of the fourth, retained intrinsic height at three or fewer, and preserved overflow fades · ui/src/pages/chat/components/chat-composer-queue.ts, ui/src/styles/components.css · COMPONENT T4 queue | BENCH
2026-08-22 12:42 · Added Goal, Goal plus queue, Commands open, Skills open, Plus menu open, New empty and New incognito to the frequency-grouped review sweep · ui/src/test-helpers/composer-bench.ts · COMPONENT composer sweep coverage | BENCH
2026-08-22 12:42 · Deepened Goal and queue underlap and aligned their full-width 20px superellipse chrome with the real composer · ui/src/styles/chat/layout.css, ui/src/styles/components.css · COMPONENT attached composer surfaces | BENCH
2026-08-22 12:42 · Replaced the Goal target glyph with the canonical Lucide Goal icon through the shared icon registry · ui/src/components/icons.ts, ui/src/pages/chat/components/chat-composer-goal.ts · COMPONENT goal | BENCH
2026-08-22 12:42 · Removed Task suggestion from the bench controls and sweep only, leaving the product component and its styles untouched · scripts/control-ui-mock-dev.ts, ui/src/test-helpers/composer-bench.ts · COMPONENT bench scope | BENCH
2026-08-22 12:42 · Removed the redundant Fast mode tooltip because the visible subtitle already owns the explanation · ui/src/pages/chat/components/chat-effort-picker.ts · COMPONENT effort picker | BENCH
2026-08-22 12:42 · Representable state inventory: Chat or New surface; normal, Draft or incognito visibility; empty, one-line, multiline, atomic skill and giant content; image, annotation or mixed attachments; slash, skills and capability menus; idle, running, steering, approval and interrupted runs; none, 1, 3 or 6 queue rows across text, attachment, command, member and run-attached types with reconnect, delivery and edit states; none, 1 or 3 tasks; inactive, active or complete Plan; Goal, reply, progress, compaction, fallback, archived, recovery, model setup and question insets; permission, model, reasoning, Fast mode, voice, dictation, health, access, width and theme axes; session suggestion, pull request, swarm, disk, conflict, placement and chat-error neighbors · scripts/control-ui-mock-dev.ts, ui/src/test-helpers/composer-bench.ts · COMPONENT state inventory | BENCH
2026-08-22 12:42 · Plausible states still outside the sweep: single queue row; queue inline edit and reordered drag; attachment, command, member and run-attached queue rows; tasks alone with 1 or 3; offline without queue; reconnecting or degraded health; disabled or catalog-unavailable composer; image-only, annotation-only and giant prompt; permission, model, effort and microphone pickers open; New blocked, locked, invalid-worktree, outcome-unknown and recovery outcomes; reply, compaction, fallback, archived, recovery, model-setup and question insets; mobile and light variants of the primary composite states · ui/src/test-helpers/composer-bench.ts · COMPONENT sweep follow-up inventory | BENCH
+14
View File
@@ -152,3 +152,17 @@ Local-only working ledger. Re-read after every implementation block; move an ite
- [x] The review sweep covers plausible grouped product states, including a real open 40-skill sheet and an inline multiline skill.
- [x] Queue, goal and status bands use the single `--composer-attached-surface` hierarchy token in both themes.
- [x] Plan hover card stays still through its close delay, exits once over 100ms, cancels cleanly on reentry, and uses the compact 352px chrome.
- [x] Align Running tasks and Plan on one left-anchored row only when both coexist; preserve standalone alignment.
- [x] Split Model and Effort into independent footer controls with separate chevrons, hover and menus.
- [x] Restore offline and members bands with item-owned tint, border and canonical icons.
- [x] Replace explicit skill-fixture newlines with natural desktop and mobile wrapping.
- [x] Remove Interrupted and approval cards from the composer sweep without touching their product owners.
- [x] Restore the prior full-width dictation waveform and change only the edge orbit to 5s linear motion.
- [x] Keep Plan progress neutral and success color on checks only.
- [x] Show 3.4 queue rows when scrollable and intrinsic height with three or fewer rows.
- [x] Add Goal, Goal plus queue, open command, open skill, open plus, New empty and New incognito sweep steps.
- [x] Match Goal and queue width, radius, superellipse and underlap to the composer.
- [x] Use Lucide Goal for Goal and Lucide Globe Off for Offline through the shared registry.
- [x] Remove Task suggestion from the bench only.
- [x] Remove the duplicate Fast mode tooltip.
- [x] Record every representable bench axis and the plausible combinations still absent from the sweep.
-1
View File
@@ -383,7 +383,6 @@ const composerBenchHtml = `<!doctype html>
[
["none", "None"],
["approval", "Approval"],
["task-suggestion", "Task suggestion"],
["session-suggestion", "Session suggestion"],
["pull-request", "Pull request"],
["swarm", "Swarm progress"],
+12
View File
@@ -185,6 +185,9 @@ export const icons = {
target: strokeIcon(svg` <circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="6" />
<circle cx="12" cy="12" r="2" />`),
goal: strokeIcon(svg` <path d="M12 13V2l8 4-8 4" />
<path d="M20.561 10.222a9 9 0 1 1-12.55-5.29" />
<path d="M8.002 9.997a5 5 0 1 0 8.9 2.02" />`),
switchCamera: strokeIcon(svg` <path d="M11 19H6.5a4.5 4.5 0 0 1 0-9H8" />
<path d="m8 16 3 3-3 3" />
<path d="M13 5h4.5a4.5 4.5 0 0 1 0 9H16" />
@@ -307,6 +310,15 @@ export const icons = {
<path d="M5 5a10 10 0 0 1 14 0" />
<path d="M12 18h.01" />
<path d="M8.5 15.5a5 5 0 0 1 2-1.2" />`),
globeOff: strokeIcon(svg` <path
d="M10.114 4.462A14.5 14.5 0 0 1 12 2a10 10 0 0 1 9.313 13.643"
/>
<path d="M15.557 15.556A14.5 14.5 0 0 1 12 22 10 10 0 0 1 4.929 4.929" />
<path d="M15.892 10.234A14.5 14.5 0 0 0 12 2a10 10 0 0 0-3.643.687" />
<path d="M17.656 12H22" />
<path d="M19.071 19.071A10 10 0 0 1 12 22 14.5 14.5 0 0 1 8.44 8.45" />
<path d="M2 12h10" />
<path d="m2 2 20 20" />`),
...toolIcons,
} as const;
@@ -78,6 +78,10 @@ function renderStatusPreview(remainingTasks: readonly TaskSummary[]): TemplateRe
const overflow = tasks.length - preview.length;
return html`
<div slot="content" class="chat-tasks-preview">
<div class="chat-tasks-preview__heading">
<span>${t("chat.backgroundTasks.title")}</span>
<span>${t("chat.backgroundTasks.running", { count: String(active.length) })}</span>
</div>
${preview.map((task) => renderStatusPreviewRow(task))}
${overflow > 0
? html`<div class="chat-tasks-preview__more">
@@ -103,7 +103,7 @@ export function renderChatGoal(
aria-label=${formatGoalDetail(goal)}
>
<div class="agent-chat__goal-row">
<span class="agent-chat__goal-icon">${icons.target}</span>
<span class="agent-chat__goal-icon">${icons.goal}</span>
<span class="agent-chat__goal-copy">
<span class="agent-chat__goal-label">${formatGoalStatusLabel(goal.status)}</span>
<span class="agent-chat__goal-objective">${goal.objective}</span>
@@ -152,6 +152,7 @@ export function renderChatQueue(props: ChatQueueProps) {
class="chat-queue__scroll"
data-scrollable=${visibleQueue.length > 3 ? "true" : "false"}
data-at-start="true"
data-at-end=${visibleQueue.length > 3 ? "false" : "true"}
@dragover=${(event: DragEvent) => {
if (!event.dataTransfer?.types.includes(DRAG_MIME)) return;
const container = event.currentTarget;
@@ -175,6 +176,9 @@ export function renderChatQueue(props: ChatQueueProps) {
const scroll = event.currentTarget;
if (scroll instanceof HTMLElement) {
scroll.dataset.atStart = String(scroll.scrollTop <= 1);
scroll.dataset.atEnd = String(
scroll.scrollTop + scroll.clientHeight >= scroll.scrollHeight - 1,
);
}
}}
>
@@ -202,7 +202,7 @@ export function renderChatComposerView(context: ChatComposerViewContext) {
{
text: offlineText,
tone: "warn" as const,
icon: icons.wifiOff,
icon: icons.globeOff,
omitsSecondary: false,
},
]
@@ -102,9 +102,6 @@ export function renderChatEffortPicker(params: ChatEffortPickerParams) {
};
const onlyStop = sliderStops.length === 1 ? sliderStops[0] : undefined;
const onlyStopSelected = selection.kind === "anchored" && selection.index === 0;
const speedTooltip = params.fastMode.supported
? t("chat.modelControls.fastHelp")
: t("chat.modelControls.speedUnsupported");
return html`
<details
class="chat-controls__inline-select chat-controls__effort-picker"
@@ -136,6 +133,9 @@ export function renderChatEffortPicker(params: ChatEffortPickerParams) {
? html`<span class="chat-controls__effort-zap" aria-hidden="true">${icons.zap}</span>`
: nothing}
<span class="chat-controls__inline-select-label">${triggerLabel}</span>
<span class="chat-controls__inline-select-chevron" aria-hidden="true"
>${icons.chevronUp}</span
>
</summary>
<wa-popup data-anchored-overlay>
<div
@@ -263,31 +263,29 @@ export function renderChatEffortPicker(params: ChatEffortPickerParams) {
${t("chat.modelControls.fastHelp")}
</span>
</span>
<openclaw-tooltip .content=${speedTooltip}>
<button
class="chat-controls__speed-toggle ${params.fastMode.active
? "chat-controls__speed-toggle--active"
: ""}"
data-chat-speed-toggle=${params.fastMode.nextValue}
type="button"
role="switch"
aria-checked=${params.fastMode.active ? "true" : "false"}
aria-label=${t("chat.modelControls.fastResponsesAria", {
state: params.fastMode.label,
})}
?disabled=${params.fastMode.disabled}
@click=${(event: MouseEvent) => {
event.stopPropagation();
if (params.fastMode.disabled) {
event.preventDefault();
return;
}
commitFastMode(params.fastMode.nextValue);
}}
>
<span class="chat-controls__speed-toggle-thumb"></span>
</button>
</openclaw-tooltip>
<button
class="chat-controls__speed-toggle ${params.fastMode.active
? "chat-controls__speed-toggle--active"
: ""}"
data-chat-speed-toggle=${params.fastMode.nextValue}
type="button"
role="switch"
aria-checked=${params.fastMode.active ? "true" : "false"}
aria-label=${t("chat.modelControls.fastResponsesAria", {
state: params.fastMode.label,
})}
?disabled=${params.fastMode.disabled}
@click=${(event: MouseEvent) => {
event.stopPropagation();
if (params.fastMode.disabled) {
event.preventDefault();
return;
}
commitFastMode(params.fastMode.nextValue);
}}
>
<span class="chat-controls__speed-toggle-thumb"></span>
</button>
</div>
`
: nothing}
@@ -392,12 +392,6 @@ export function renderChatModelControls(props: ChatModelControlsProps) {
onTargetSelect: props.onModelPickerTargetSelect,
onRequestUpdate: props.onRequestUpdate,
})}
<!-- Model and reasoning describe one decision, so they read as one phrase
with a separator between them. It stays outside both triggers: each
keeps its own hover and click target, and the mark itself is
decorative, so it is hidden from assistive technology rather than
announced between two controls that already name themselves. -->
<span class="chat-controls__model-settings-separator" aria-hidden="true">·</span>
${renderChatEffortPicker({
disabled: effortDisabled,
disabledReason: props.effortMutationDisabledReason,
@@ -440,6 +440,9 @@ export function renderChatModelPicker(params: ChatModelPickerParams) {
<span class="chat-controls__inline-select-label">
${params.triggerStatusLabel ?? params.triggerModelLabel}
</span>
<span class="chat-controls__inline-select-chevron" aria-hidden="true"
>${icons.chevronUp}</span
>
</summary>
<wa-popup data-anchored-overlay>
<div
+40 -62
View File
@@ -1034,14 +1034,13 @@ openclaw-chat-page {
}
.agent-chat__goal-float {
--chat-goal-underlap: 14px;
--chat-goal-underlap: 18px;
position: relative;
z-index: 0;
box-sizing: border-box;
width: 100%;
margin: 0
calc(0px - var(--chat-composer-shell-gap) - var(--chat-goal-underlap));
margin: 0 0 calc(0px - var(--chat-composer-shell-gap) - var(--chat-goal-underlap));
}
.agent-chat__goal-float .agent-chat__goal {
@@ -1052,8 +1051,9 @@ openclaw-chat-page {
padding-top: 0;
padding-bottom: var(--chat-goal-underlap);
border-bottom: 0;
border-radius: calc(16px * var(--openclaw-corner-radius-scale))
calc(16px * var(--openclaw-corner-radius-scale)) 0 0;
border-radius: calc(20px * var(--openclaw-corner-radius-scale))
calc(20px * var(--openclaw-corner-radius-scale)) 0 0;
corner-shape: superellipse(1.5);
background: var(--composer-attached-surface, var(--chat-composer-surface, var(--card)));
}
@@ -3282,7 +3282,7 @@ button.chat-pr__diff {
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: chat-dictation-edge-orbit 2.2s linear infinite;
animation: chat-dictation-edge-orbit 5s linear infinite;
pointer-events: none;
}
@@ -3371,19 +3371,19 @@ button.chat-pr__diff {
queue item, or toast owner and never stack additional composer rows here. */
.agent-chat__composer-underlaps[data-tone="info"] {
border-color: color-mix(in srgb, var(--info) 12%, transparent);
background: var(--composer-attached-surface);
background: color-mix(in srgb, var(--info) 7%, var(--composer-attached-surface));
color: color-mix(in srgb, var(--info) 72%, var(--text));
}
.agent-chat__composer-underlaps[data-tone="warn"] {
border-color: color-mix(in srgb, var(--warn) 12%, transparent);
background: var(--composer-attached-surface);
background: color-mix(in srgb, var(--warn) 7%, var(--composer-attached-surface));
color: color-mix(in srgb, var(--warn) 72%, var(--text));
}
.agent-chat__composer-underlaps[data-tone="danger"] {
border-color: color-mix(in srgb, var(--danger) 12%, transparent);
background: var(--composer-attached-surface);
background: color-mix(in srgb, var(--danger) 7%, var(--composer-attached-surface));
color: color-mix(in srgb, var(--danger) 78%, var(--text));
}
@@ -3402,12 +3402,15 @@ button.chat-pr__diff {
.agent-chat__composer-status-icon,
.agent-chat__composer-status-more {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
color: inherit;
}
.agent-chat__composer-status-icon svg {
width: var(--chat-composer-footer-icon-size);
height: var(--chat-composer-footer-icon-size);
width: var(--chat-composer-footer-icon-size, 16px);
height: var(--chat-composer-footer-icon-size, 16px);
}
.agent-chat__composer-status-text {
@@ -3764,56 +3767,8 @@ button.chat-pr__diff {
min-width: 0;
}
/* Model and reasoning live in their own row inside this group, which carries a
tighter gap of its own elsewhere in the app. Inside the composer the pair sits
on the family rhythm like every other chip, and the mark between them is what
makes it read as one phrase rather than two separate settings. */
.agent-chat__composer-controls .chat-controls__model-settings {
gap: var(--chat-composer-chip-gap);
border-radius: var(--radius-full);
background: transparent;
transition: background var(--duration-fast) ease;
}
@media (hover: hover) and (pointer: fine) {
.agent-chat__composer-controls .chat-controls__model-settings:hover {
background: var(--chat-composer-group-hover);
}
.agent-chat__composer-controls
.chat-controls__model-settings:hover
.chat-controls__inline-select-trigger,
.agent-chat__composer-controls
.chat-controls__model-settings:hover
.chat-controls__model-settings-separator {
color: var(--text-strong);
}
}
/* Only the composer binds the pair into a phrase; elsewhere the two pickers sit
in layouts that do not read as one, where the mark would be noise. */
.chat-controls__model-settings-separator {
display: none;
}
/* Decorative binder between the two triggers. Non-interactive on purpose: it
carries no pointer target of its own, so neither picker's hover or click area
is narrowed by it, and it sits at the surface's muted step so the phrase reads
as label-mark-label rather than three equal words. The negative margin pulls
it into the inset the two chips already own, which is what makes the pair read
tighter than the space separating it from the chip beside it. */
.agent-chat__composer-controls .chat-controls__model-settings-separator {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
margin-inline: -3px;
color: var(--muted);
/* A step above the chip text: the middle dot is a small mark inside its own em
box, so at the label's size it reads as a speck rather than a separator. */
font-size: calc(var(--chat-composer-chip-text, 13px) + 1px);
line-height: 1;
pointer-events: none;
user-select: none;
gap: 4px;
}
.agent-chat__composer-run-status {
@@ -3885,7 +3840,7 @@ button.chat-pr__diff {
}
.agent-chat__disabled-banner--neutral {
color: var(--text);
color: inherit;
}
.agent-chat__disabled-banner-icon {
@@ -6522,7 +6477,7 @@ button.chat-pr__diff {
.chat-controls__model-settings {
display: flex;
align-items: center;
gap: 4px;
gap: 2px;
}
.chat-controls__model-settings .chat-controls__inline-select-trigger {
@@ -6613,6 +6568,29 @@ button.chat-pr__diff {
white-space: nowrap;
}
.chat-controls__inline-select-chevron {
display: inline-flex;
width: 12px;
height: 12px;
flex: 0 0 auto;
align-items: center;
justify-content: center;
color: var(--muted);
transform: rotate(180deg);
transition: transform var(--duration-fast) ease;
}
.chat-controls__inline-select-chevron svg {
width: 12px;
height: 12px;
}
.chat-controls__inline-select[open]
> .chat-controls__inline-select-trigger
.chat-controls__inline-select-chevron {
transform: rotate(0deg);
}
.chat-controls__trigger-meta {
color: var(--muted);
flex: 0 0 auto;
+11 -4
View File
@@ -431,17 +431,24 @@
background: color-mix(in srgb, var(--text-strong) 8%, transparent);
}
.session-progress-card__progress::-webkit-progress-value,
.session-progress-card__progress::-webkit-progress-value {
border-radius: 999px;
background: color-mix(in srgb, var(--foreground, var(--text-strong)) 55%, transparent);
}
.session-progress-card__progress::-moz-progress-bar {
border-radius: 999px;
background: var(--ok);
background: color-mix(in srgb, var(--foreground, var(--text-strong)) 55%, transparent);
}
.session-progress-card--composer[data-complete="true"]
.session-progress-card__progress::-webkit-progress-value,
.session-progress-card__progress::-webkit-progress-value {
background: color-mix(in srgb, var(--foreground, var(--text-strong)) 35%, transparent);
}
.session-progress-card--composer[data-complete="true"]
.session-progress-card__progress::-moz-progress-bar {
background: color-mix(in srgb, var(--ok) 38%, transparent);
background: color-mix(in srgb, var(--foreground, var(--text-strong)) 35%, transparent);
}
.session-progress-card--composer .session-progress-card__markdown progress {
+31 -6
View File
@@ -2125,19 +2125,43 @@
tooltip body is content-sized (width: max-content up to --max-width), so a
single task stays one compact line instead of a fixed-size panel. */
openclaw-tooltip.chat-tasks-status__preview {
--openclaw-tooltip-max-width: min(360px, calc(100vw - 24px));
--openclaw-tooltip-max-width: min(352px, calc(100vw - 24px));
--openclaw-tooltip-background-color: var(--chat-composer-surface, var(--popover));
--openclaw-tooltip-border-color: color-mix(in srgb, var(--text-strong) 11%, transparent);
--openclaw-tooltip-border-radius: calc(16px * var(--openclaw-corner-radius-scale));
--openclaw-tooltip-padding: 11px;
--openclaw-tooltip-shadow: 0 1px 2px rgb(0 0 0 / 8%), 0 12px 32px rgb(0 0 0 / 14%);
}
.chat-tasks-preview {
display: flex;
width: min(330px, calc(100vw - 46px));
flex-direction: column;
gap: 6px;
gap: 7px;
color: var(--muted);
font-size: var(--control-ui-text-xs);
line-height: 1.4;
text-align: left;
}
.chat-tasks-preview__heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
margin-bottom: 1px;
color: var(--muted);
font-size: var(--control-ui-text-xs);
font-weight: 650;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.chat-tasks-preview__row {
display: flex;
display: grid;
grid-template-columns: 14px minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
gap: 7px;
}
.chat-tasks-preview__title {
@@ -2146,8 +2170,8 @@ openclaw-tooltip.chat-tasks-status__preview {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text);
font-weight: 550;
color: var(--muted);
font-weight: 400;
}
.chat-tasks-preview__meta {
@@ -2156,6 +2180,7 @@ openclaw-tooltip.chat-tasks-status__preview {
align-items: baseline;
gap: 5px;
color: var(--muted);
font-size: inherit;
font-variant-numeric: tabular-nums;
}
+20 -5
View File
@@ -2914,7 +2914,7 @@ td.data-table-key-col {
/* Queued turns sit behind the composer's rounded top edge. The inset keeps the
queue visibly subordinate; the protected bottom space is what may underlap. */
.chat-queue {
--chat-queue-underlap: 14px;
--chat-queue-underlap: 18px;
--chat-queue-row-height: 42px;
--chat-queue-hairline: color-mix(in srgb, var(--text-strong) 16%, transparent);
@@ -2931,8 +2931,9 @@ td.data-table-key-col {
padding-bottom: var(--chat-queue-underlap);
border: 1px solid var(--chat-queue-hairline);
border-bottom: 0;
border-radius: calc(16px * var(--openclaw-corner-radius-scale))
calc(16px * var(--openclaw-corner-radius-scale)) 0 0;
border-radius: calc(20px * var(--openclaw-corner-radius-scale))
calc(20px * var(--openclaw-corner-radius-scale)) 0 0;
corner-shape: superellipse(1.5);
background: var(--composer-attached-surface, var(--chat-composer-surface));
}
@@ -2944,15 +2945,29 @@ td.data-table-key-col {
}
.chat-queue__scroll[data-scrollable="true"] {
height: calc(3 * var(--chat-queue-row-height));
height: calc(3.4 * var(--chat-queue-row-height));
overflow-y: auto;
overscroll-behavior: contain;
}
.chat-queue__scroll[data-scrollable="true"][data-at-start="false"] {
.chat-queue__scroll[data-scrollable="true"][data-at-start="true"][data-at-end="false"] {
mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 22px), transparent 100%);
}
.chat-queue__scroll[data-scrollable="true"][data-at-start="false"][data-at-end="true"] {
mask-image: linear-gradient(to bottom, transparent 0, #000 16px, #000 100%);
}
.chat-queue__scroll[data-scrollable="true"][data-at-start="false"][data-at-end="false"] {
mask-image: linear-gradient(
to bottom,
transparent 0,
#000 16px,
#000 calc(100% - 22px),
transparent 100%
);
}
.chat-queue__scroll::-webkit-scrollbar {
width: 5px;
}
+31 -8
View File
@@ -502,31 +502,49 @@ body {
align-self: start;
justify-self: start;
width: auto;
padding-top: 8px;
margin: 0;
padding-top: 0;
margin: 8px 0 0 var(--composer-bench-composer-inset, 0px);
}
.composer-bench__composer-stack[data-shared-status-row] .chat-tasks-status {
margin: 0;
}
.composer-bench__composer-stack[data-shared-status-row] .agent-chat__progress-float {
width: auto;
justify-self: start;
justify-items: start;
margin-left: var(--composer-bench-tasks-offset, 0px);
}
.composer-bench__tasks-status {
box-sizing: border-box;
width: 100%;
max-width: var(--chat-thread-max-width, 48rem);
padding-inline: 12px;
margin: 0 auto 8px;
width: fit-content;
max-width: 100%;
padding-inline: 0;
margin: 0 0 8px;
}
.composer-bench__tasks-status .chat-tasks-status {
margin: 0;
}
.composer-bench__tasks-status .chat-tasks-status__time,
.composer-bench__tasks-status .chat-tasks-status__link {
color: var(--text);
font-size: 12px;
line-height: 14px;
}
.composer-bench__tasks-status .chat-tasks-status__sep {
color: var(--muted);
}
.composer-bench__neighbor {
position: relative;
box-sizing: border-box;
width: 100%;
margin-bottom: 8px;
width: min(100%, var(--composer-bench-composer-width, 768px));
margin: 0 auto 8px;
}
.composer-bench__neighbor > * {
@@ -538,6 +556,11 @@ body {
margin: 0;
}
.composer-bench__neighbor > .exec-approval-card {
width: 100%;
max-width: 100%;
}
@media (prefers-reduced-motion: reduce) {
.composer-bench__scenario-nav button,
.composer-bench__choice--disclosure > summary::after,
+83 -52
View File
@@ -23,7 +23,6 @@ import { renderChatPermissionPicker } from "../pages/chat/components/chat-permis
import { renderChatPullRequests } from "../pages/chat/components/chat-pull-requests.ts";
import { renderChatSessionSuggestions } from "../pages/chat/components/chat-session-suggestions.ts";
import { renderChatSwarmProgress } from "../pages/chat/components/chat-swarm-progress.ts";
import { renderChatTaskSuggestionTray } from "../pages/chat/components/chat-task-suggestions.ts";
import { renderWelcomeState } from "../pages/chat/components/chat-welcome.ts";
import { renderChatViewNotices } from "../pages/chat/chat-view-notices.ts";
import { RealtimeTalkLevelSignal } from "../pages/chat/realtime-talk-level.ts";
@@ -106,9 +105,10 @@ type BenchState = {
| "busy"
| "inactive"
| "failed";
content: "empty" | "one" | "multiline" | "skill-inline" | "giant";
content: "empty" | "one" | "multiline" | "skill-inline" | "skill-inline-mobile" | "giant";
attachments: "none" | "image" | "annotation" | "mixed";
menu: "closed" | "slash" | "skills";
plusMenuOpen: boolean;
status: "focused" | "sending" | "disabled" | "catalog" | "error" | "offline";
newAction:
| "start"
@@ -122,7 +122,6 @@ type BenchState = {
neighbor:
| "none"
| "approval"
| "task-suggestion"
| "session-suggestion"
| "pull-request"
| "swarm"
@@ -186,6 +185,7 @@ const defaults: BenchState = {
content: "multiline",
attachments: "none",
menu: "closed",
plusMenuOpen: false,
status: "focused",
newAction: "start",
neighbor: "none",
@@ -198,7 +198,9 @@ const content: Record<BenchState["content"], string> = {
one: "Tailor the composer spacing.",
multiline: "Tailor the composer spacing.\nKeep the controls aligned.\nPreserve the real send flow.",
"skill-inline":
"Tailor the composer spacing with $bench_skill_05 before the final pass,\nthen re-check the send flow.",
"Tailor the composer spacing with deliberate care across every footer control before using $bench_skill_05 to finish the final visual pass and re-check the complete send flow.",
"skill-inline-mobile":
"Review with $bench_skill_05 before checking the send flow again on a compact mobile surface.",
giant: Array.from(
{ length: 18 },
(_, index) => `Line ${index + 1}: inspect wrapping, internal scroll, and footer stability.`,
@@ -210,7 +212,9 @@ function seededDraft(
menu: BenchState["menu"],
): string {
if (menu === "slash") return "/";
if (menu === "skills") return "Use $bench_";
if (menu === "skills") {
return contentKey === "one" ? "Use $bench_skill_0" : "Use $bench_";
}
return content[contentKey];
}
@@ -377,7 +381,7 @@ const presets: Record<string, Partial<BenchState>> = {
status: "focused",
width: 900,
},
"skills-40": { content: "one", menu: "skills", run: "idle", queue: "none", width: 640 },
"skills-40": { content: "empty", menu: "skills", run: "idle", queue: "none", width: 640 },
"mobile-full": {
content: "multiline",
attachments: "image",
@@ -422,7 +426,7 @@ const scenarios: BenchScenario[] = [
state: { content: "one", status: "sending", run: "running" },
},
{
name: "Plan 1 of 3",
name: "Plan 2 of 3",
description: "The current plan sits above the composer as work progresses.",
state: { content: "multiline", plan: "active" },
},
@@ -436,6 +440,16 @@ const scenarios: BenchScenario[] = [
description: "The final completed step uses the success state.",
state: { content: "one", plan: "complete" },
},
{
name: "Goal",
description: "A session goal sits directly above the composer.",
state: { content: "one", inset: "goal" },
},
{
name: "Goal + queue",
description: "The queue stacks above the goal in the canonical attached order.",
state: { content: "one", run: "running", queue: "three", inset: "goal" },
},
{
name: "Queue with 3",
description: "Three queued messages remain attached to the composer.",
@@ -463,23 +477,28 @@ const scenarios: BenchScenario[] = [
},
{
name: "Skill inline",
description: "An atomic skill mention remains inline across a multiline draft.",
description: "A skill mention wraps naturally without an explicit newline.",
state: { content: "skill-inline" },
},
{
name: "Interrupted",
description: "The terminal event stays with the last transcript message.",
state: { content: "one", run: "interrupted" },
name: "Skill inline mobile",
description: "The same atomic mention wraps naturally on a narrow surface.",
state: { content: "skill-inline-mobile", width: 390 },
},
{
name: "Approval pending",
description: "The approval card is the only pending-approval state.",
state: { content: "one", run: "approval", neighbor: "approval" },
name: "Commands open",
description: "The real slash-command palette opens with repository commands.",
state: { content: "empty", menu: "slash" },
},
{
name: "Approval + offline",
description: "Approval actions pause while offline owns the status band.",
state: { content: "one", run: "approval", status: "offline", neighbor: "approval" },
name: "Skills open",
description: "The real skill invocation sheet opens for normal navigation.",
state: { content: "one", menu: "skills" },
},
{
name: "Plus menu open",
description: "The real capability menu opens from the composer attachment control.",
state: { content: "one", capabilities: "available", plusMenuOpen: true },
},
{
name: "Read only",
@@ -492,9 +511,14 @@ const scenarios: BenchScenario[] = [
state: { content: "one", dictate: "recording" },
},
{
name: "Task suggestion",
description: "A proposed follow-up stays aligned with the composer.",
state: { content: "one", neighbor: "task-suggestion" },
name: "New session",
description: "The New surface begins with an empty real composer.",
state: { surface: "new", content: "empty" },
},
{
name: "New incognito",
description: "A temporary New session disappears after Gateway restart.",
state: { surface: "new", visibility: "incognito", content: "empty" },
},
{
name: "Session suggestion",
@@ -557,12 +581,6 @@ const scenarios: BenchScenario[] = [
theme: "light",
},
},
{
name: "Incognito session",
description: "A temporary New session that disappears after Gateway restart.",
state: presets.incognito ?? {},
stress: true,
},
{
name: "Huge prompt + attachments",
description: "A long running draft stress-tests scroll and attachment overflow.",
@@ -724,7 +742,14 @@ function readState(raw: string | null): BenchState {
);
const menu = parsed.menu === "slash" || parsed.menu === "skills" ? parsed.menu : "closed";
const surface = parsed.surface === "new" ? "new" : "chat";
const contentKey = ["empty", "one", "multiline", "giant"].includes(String(parsed.content))
const contentKey = [
"empty",
"one",
"multiline",
"skill-inline",
"skill-inline-mobile",
"giant",
].includes(String(parsed.content))
? (parsed.content as BenchState["content"])
: surface === "new"
? "empty"
@@ -1122,31 +1147,6 @@ function renderBenchNeighbor() {
variant: "inline",
onDecision: clear,
});
} else if (state.neighbor === "task-suggestion") {
surface = renderChatTaskSuggestionTray({
taskSuggestions: [
{
id: "bench-task-suggestion",
title: "Review composer interactions",
prompt: "Audit the existing composer interactions and report concrete regressions.",
tldr: "Check the current composer behavior before the visual pass.",
cwd: "/workspace/openclaw",
sessionKey: "agent:main:main",
agentId: "main",
createdAt: Date.now(),
},
],
taskSuggestionBusyIds: new Set(),
taskSuggestionCloudProfiles: [],
taskSuggestionCopiedIds: new Set(),
canAcceptTaskSuggestions: true,
canAcceptTaskSuggestionModes: true,
canDismissTaskSuggestions: true,
onAcceptTaskSuggestion: clear,
onDismissTaskSuggestion: clear,
onCopyTaskSuggestionPrompt: () => {},
onNavigateTaskSuggestion: () => {},
});
} else if (state.neighbor === "session-suggestion") {
surface = renderChatSessionSuggestions({
suggestions: [
@@ -1293,6 +1293,10 @@ function renderChatSurface(sessionList: SessionsListResult) {
// stub alive across rerenders. Clearing only our own stub (marked) lets the
// real controller path own the slot whenever the axis is off.
const composerState = getChatComposerState("composer-bench");
composerState.capabilityMenuOpen = state.plusMenuOpen;
if (state.plusMenuOpen) {
composerState.capabilityMenuView = "root";
}
if (state.dictate !== "off") {
const stub = benchDictationController() as ComposerDictationController & {
benchStub?: boolean;
@@ -1862,6 +1866,33 @@ function renderBench(): void {
}
function applyTransientState(): void {
const sharedStack = document.querySelector<HTMLElement>(
".composer-bench__composer-stack[data-shared-status-row]",
);
const tasksStatus = sharedStack?.querySelector<HTMLElement>(".chat-tasks-status");
if (sharedStack && tasksStatus) {
const composerShell = sharedStack.querySelector<HTMLElement>(".agent-chat__composer-shell");
const stackRect = sharedStack.getBoundingClientRect();
const composerRect = composerShell?.getBoundingClientRect();
if (composerRect) {
sharedStack.style.setProperty(
"--composer-bench-composer-inset",
`${Math.max(0, Math.round(composerRect.left - stackRect.left))}px`,
);
}
sharedStack.style.setProperty(
"--composer-bench-tasks-offset",
`${Math.ceil(tasksStatus.getBoundingClientRect().width + 8)}px`,
);
}
const surface = document.querySelector<HTMLElement>(".composer-bench__surface");
const composerShell = surface?.querySelector<HTMLElement>(".agent-chat__composer-shell");
if (surface && composerShell) {
surface.style.setProperty(
"--composer-bench-composer-width",
`${Math.round(composerShell.getBoundingClientRect().width)}px`,
);
}
const textarea = document.querySelector<HTMLTextAreaElement>(".agent-chat__composer-combobox textarea");
if (!textarea) {
return;