mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
feat(ui): complete composer bench state sweep
This commit is contained in:
@@ -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 card’s 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user