mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-14 09:56:07 +00:00
improve(ui): unify the New session draft with the start-screen layout (#105069)
The /new draft page now renders the same welcome block as the empty-chat start screen, with the draft controls folded in launcher-style: hero on top, a quiet borderless target row (agent, node, folder, worktree) directly above the mid-screen composer, and the recent chats below. The page reuses renderWelcomeState via hint/composer slots so the surfaces cannot drift apart; the shell treats /new as a chat-like route that owns its scrolling, with the top inset doubling as the native titlebar drag band. The welcome is content-sized on this route so a tall draft (open folder browser, short window) stays scrollable from the top. Also repairs the stale new-session e2e heading wait (the h1 was removed in Closes #105068
This commit is contained in:
committed by
GitHub
parent
2af4871f63
commit
fecd1ca195
@@ -918,6 +918,9 @@ class OpenClawShell extends OpenClawLightDomElement {
|
||||
const shellWidth = Math.max(globalThis.innerWidth || 0, NAV_WIDTH_MAX);
|
||||
// One storage read per render; theme.refresh() re-renders on pref changes.
|
||||
const uiSettings = loadSettings();
|
||||
// The new-session draft shares the chat layout: full-height pane that owns
|
||||
// its scrolling and pins the composer dock to the bottom.
|
||||
const chatLikeRoute = activeRoute === "chat" || activeRoute === "new-session";
|
||||
return html`
|
||||
<openclaw-command-palette
|
||||
.onNavigate=${(routeId: RouteId) => this.navigate(routeId)}
|
||||
@@ -928,7 +931,7 @@ class OpenClawShell extends OpenClawLightDomElement {
|
||||
.onSlashCommand=${this.handleCommandPaletteSlashCommand}
|
||||
></openclaw-command-palette>
|
||||
<div
|
||||
class="shell ${activeRoute === "chat" ? "shell--chat" : ""} ${navCollapsed
|
||||
class="shell ${chatLikeRoute ? "shell--chat" : ""} ${navCollapsed
|
||||
? "shell--nav-collapsed"
|
||||
: ""} ${navDrawerOpen ? "shell--nav-drawer-open" : ""} ${this.onboarding
|
||||
? "shell--onboarding"
|
||||
@@ -1046,8 +1049,7 @@ class OpenClawShell extends OpenClawLightDomElement {
|
||||
`
|
||||
: nothing}
|
||||
<main
|
||||
class="content ${activeRoute === "chat" ? "content--chat" : ""} ${activeRoute ===
|
||||
"workboard"
|
||||
class="content ${chatLikeRoute ? "content--chat" : ""} ${activeRoute === "workboard"
|
||||
? "content--workboard"
|
||||
: ""}"
|
||||
tabindex="-1"
|
||||
|
||||
@@ -101,8 +101,25 @@ describeControlUiE2e("Control UI new-session page mocked Gateway E2E", () => {
|
||||
// "+" navigates to the same route with ?agent=<id>).
|
||||
const response = await page.goto(`${server.baseUrl}new`);
|
||||
expect(response?.status()).toBe(200);
|
||||
// The draft page shows the start-screen welcome hero for the agent.
|
||||
await page.getByRole("heading", { name: "Main" }).waitFor();
|
||||
await page.locator(".new-session-page__message").waitFor();
|
||||
|
||||
// Unified layout: the draft block (target row above the composer) sits
|
||||
// inside the start-screen welcome, below the hero.
|
||||
const heroBox = await page.locator(".agent-chat__welcome h2").boundingBox();
|
||||
const targetsBox = await page.locator(".new-session-page__targets").boundingBox();
|
||||
const composerBox = await page.locator(".new-session-page__composer").boundingBox();
|
||||
expect(heroBox).not.toBeNull();
|
||||
expect(targetsBox).not.toBeNull();
|
||||
expect(composerBox).not.toBeNull();
|
||||
expect((heroBox?.y ?? 0) + (heroBox?.height ?? 0)).toBeLessThanOrEqual(
|
||||
(targetsBox?.y ?? 0) + 1,
|
||||
);
|
||||
expect((targetsBox?.y ?? 0) + (targetsBox?.height ?? 0)).toBeLessThanOrEqual(
|
||||
(composerBox?.y ?? 0) + 1,
|
||||
);
|
||||
|
||||
const folderInput = page.getByRole("textbox", { name: "Folder", exact: true });
|
||||
await expect.poll(() => folderInput.inputValue()).toBe(WORKSPACE);
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ type ChatWelcomeProps = {
|
||||
assistantName: string;
|
||||
assistantAvatar: string | null;
|
||||
assistantAvatarUrl?: string | null;
|
||||
/** Hero hint override; defaults to the chat slash-command hint. */
|
||||
hint?: unknown;
|
||||
/** Rendered between the hero and the recents (the new-session draft composer). */
|
||||
composer?: unknown;
|
||||
sessions?: SessionsListResult | null;
|
||||
sessionKey?: string;
|
||||
sessionHost?: UiSessionDefaultsHost | null;
|
||||
@@ -67,7 +71,7 @@ export function resolveAssistantDisplayAvatar(
|
||||
* minus channel-originated sessions — those live in their channel sections and
|
||||
* are not something the user "starts" from here.
|
||||
*/
|
||||
export function selectWelcomeRecentSessions(
|
||||
function selectWelcomeRecentSessions(
|
||||
props: Pick<ChatWelcomeProps, "sessions" | "sessionKey" | "sessionHost">,
|
||||
): GatewaySessionRow[] {
|
||||
if (!props.sessions) {
|
||||
@@ -110,7 +114,7 @@ function renderWelcomeClawd() {
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderWelcomeRecentSessions(
|
||||
function renderWelcomeRecentSessions(
|
||||
rows: GatewaySessionRow[],
|
||||
onOpenSession: ((sessionKey: string) => void) | undefined,
|
||||
) {
|
||||
@@ -133,9 +137,7 @@ export function renderWelcomeRecentSessions(
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderWelcomeSuggestions(
|
||||
props: Pick<ChatWelcomeProps, "onDraftChange" | "onSend">,
|
||||
) {
|
||||
function renderWelcomeSuggestions(props: Pick<ChatWelcomeProps, "onDraftChange" | "onSend">) {
|
||||
return html`
|
||||
<div class="agent-chat__suggestions">
|
||||
${WELCOME_SUGGESTION_KEYS.map((key) => {
|
||||
@@ -157,8 +159,7 @@ export function renderWelcomeSuggestions(
|
||||
`;
|
||||
}
|
||||
|
||||
/** Shared hero (avatar, name, hint) for the chat welcome and the new-session draft. */
|
||||
export function renderWelcomeHero(
|
||||
function renderWelcomeHero(
|
||||
props: Pick<ChatWelcomeProps, "assistantName" | "assistantAvatar" | "assistantAvatarUrl"> & {
|
||||
hint: unknown;
|
||||
},
|
||||
@@ -179,6 +180,7 @@ export function renderWelcomeHero(
|
||||
`;
|
||||
}
|
||||
|
||||
/** The start-screen welcome block, shared by the empty chat and the new-session draft. */
|
||||
export function renderWelcomeState(props: ChatWelcomeProps) {
|
||||
const recentSessions = selectWelcomeRecentSessions(props);
|
||||
|
||||
@@ -188,10 +190,13 @@ export function renderWelcomeState(props: ChatWelcomeProps) {
|
||||
assistantName: props.assistantName,
|
||||
assistantAvatar: props.assistantAvatar,
|
||||
assistantAvatarUrl: props.assistantAvatarUrl,
|
||||
hint: html`${t("chat.welcome.hintBeforeShortcut")} <kbd>/</kbd> ${t(
|
||||
"chat.welcome.hintAfterShortcut",
|
||||
)}`,
|
||||
hint:
|
||||
props.hint ??
|
||||
html`${t("chat.welcome.hintBeforeShortcut")} <kbd>/</kbd> ${t(
|
||||
"chat.welcome.hintAfterShortcut",
|
||||
)}`,
|
||||
})}
|
||||
${props.composer ?? nothing}
|
||||
${recentSessions.length > 0
|
||||
? renderWelcomeRecentSessions(recentSessions, props.onOpenSession)
|
||||
: renderWelcomeSuggestions(props)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { html, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { FsListDirResult } from "../../../../packages/gateway-protocol/src/index.js";
|
||||
import { applicationContext, type ApplicationContext } from "../../app/context.ts";
|
||||
import { beginNativeWindowDragFromTopInset } from "../../app/native-window-drag.ts";
|
||||
import { hasOperatorAdminAccess } from "../../app/operator-access.ts";
|
||||
import { loadSettings } from "../../app/settings.ts";
|
||||
import { icons } from "../../components/icons.ts";
|
||||
@@ -15,12 +16,7 @@ import { buildAgentMainSessionKey, normalizeAgentId } from "../../lib/sessions/s
|
||||
import { normalizeOptionalString } from "../../lib/string-coerce.ts";
|
||||
import { OpenClawLightDomElement } from "../../lit/openclaw-element.ts";
|
||||
import { SubscriptionsController } from "../../lit/subscriptions-controller.ts";
|
||||
import {
|
||||
renderWelcomeHero,
|
||||
renderWelcomeRecentSessions,
|
||||
renderWelcomeSuggestions,
|
||||
selectWelcomeRecentSessions,
|
||||
} from "../chat/components/chat-welcome.ts";
|
||||
import { renderWelcomeState } from "../chat/components/chat-welcome.ts";
|
||||
import { buildDraftSessionCreateParams } from "./create-params.ts";
|
||||
|
||||
type NewSessionRouteData = { agentId?: string };
|
||||
@@ -711,26 +707,35 @@ class NewSessionPage extends OpenClawLightDomElement {
|
||||
`;
|
||||
}
|
||||
|
||||
/** Same hero as the chat welcome screen, keyed to the draft's selected agent. */
|
||||
private renderHero() {
|
||||
const agent = this.selectedAgent();
|
||||
const identity = agent?.identity;
|
||||
/** Target row + composer, rendered mid-screen between the hero and recents. */
|
||||
private renderDraftBlock() {
|
||||
const worktreeNameInvalid =
|
||||
this.worktree &&
|
||||
this.worktreeName.trim() !== "" &&
|
||||
!WORKTREE_NAME_PATTERN.test(this.worktreeName.trim());
|
||||
return html`
|
||||
<div class="agent-chat__welcome" style="--agent-color: var(--accent)">
|
||||
${renderWelcomeHero({
|
||||
assistantName: identity?.name ?? agent?.name ?? agent?.id ?? "",
|
||||
assistantAvatar: identity?.avatar ?? identity?.emoji ?? null,
|
||||
assistantAvatarUrl: identity?.avatarUrl ?? null,
|
||||
hint: t("newSession.hint"),
|
||||
})}
|
||||
<div class="new-session-page__draft">
|
||||
${this.renderTargetBar()} ${this.renderBrowser()}
|
||||
${worktreeNameInvalid
|
||||
? html`<div class="new-session-page__error">${t("newSession.worktreeNameInvalid")}</div>`
|
||||
: nothing}
|
||||
${this.error ? html`<div class="new-session-page__error">${this.error}</div>` : nothing}
|
||||
${this.renderComposer()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/** Recent chats to jump back into, or the canned starters when none exist. */
|
||||
private renderRecents() {
|
||||
/** Same welcome block as the empty-chat start screen, keyed to the draft's agent. */
|
||||
private renderWelcome() {
|
||||
const agent = this.selectedAgent();
|
||||
const identity = agent?.identity;
|
||||
const gateway = this.context?.gateway.snapshot;
|
||||
const recents = selectWelcomeRecentSessions({
|
||||
return renderWelcomeState({
|
||||
assistantName: identity?.name ?? agent?.name ?? agent?.id ?? "",
|
||||
assistantAvatar: identity?.avatar ?? identity?.emoji ?? null,
|
||||
assistantAvatarUrl: identity?.avatarUrl ?? null,
|
||||
hint: t("newSession.hint"),
|
||||
composer: this.renderDraftBlock(),
|
||||
sessions: this.context?.sessions.state.result,
|
||||
sessionKey: buildAgentMainSessionKey({
|
||||
agentId: this.agentId || "main",
|
||||
@@ -741,37 +746,22 @@ class NewSessionPage extends OpenClawLightDomElement {
|
||||
agentsList: this.context?.agents.state.agentsList ?? null,
|
||||
hello: gateway?.hello ?? null,
|
||||
},
|
||||
});
|
||||
if (recents.length > 0) {
|
||||
return renderWelcomeRecentSessions(recents, (sessionKey) => {
|
||||
this.context?.gateway.setSessionKey(sessionKey);
|
||||
this.context?.navigate("chat", { search: searchForSession(sessionKey) });
|
||||
});
|
||||
}
|
||||
return renderWelcomeSuggestions({
|
||||
onDraftChange: (next) => {
|
||||
this.message = next;
|
||||
},
|
||||
onSend: () => void this.submit(),
|
||||
onOpenSession: (sessionKey) => {
|
||||
this.context?.gateway.setSessionKey(sessionKey);
|
||||
this.context?.navigate("chat", { search: searchForSession(sessionKey) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
const worktreeNameInvalid =
|
||||
this.worktree &&
|
||||
this.worktreeName.trim() !== "" &&
|
||||
!WORKTREE_NAME_PATTERN.test(this.worktreeName.trim());
|
||||
return html`
|
||||
<div class="new-session-page">
|
||||
<div class="new-session-page__inner">
|
||||
${this.renderHero()} ${this.renderTargetBar()} ${this.renderBrowser()}
|
||||
${worktreeNameInvalid
|
||||
? html`<div class="new-session-page__error">
|
||||
${t("newSession.worktreeNameInvalid")}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this.error ? html`<div class="new-session-page__error">${this.error}</div>` : nothing}
|
||||
${this.renderComposer()} ${this.renderRecents()}
|
||||
<div class="new-session-page__scroll" @mousedown=${beginNativeWindowDragFromTopInset}>
|
||||
${this.renderWelcome()}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -801,7 +791,7 @@ class NewSessionPage extends OpenClawLightDomElement {
|
||||
<div class="agent-chat__composer-combobox">
|
||||
<textarea
|
||||
class="new-session-page__message"
|
||||
rows="4"
|
||||
rows="3"
|
||||
placeholder=${t("newSession.messagePlaceholder")}
|
||||
.value=${this.message}
|
||||
@input=${(event: Event) => {
|
||||
|
||||
@@ -1,52 +1,78 @@
|
||||
/* Full-page new-session draft: target chips above the shared chat composer
|
||||
shell (.agent-chat__input). Corner radii stay on the composer's radius
|
||||
tokens so the draft picker matches the chat picker. */
|
||||
/* Full-page new-session draft: the start-screen welcome centers in the pane
|
||||
with the draft block (clean target row + composer) between the hero and the
|
||||
recent chats, so the draft reads as the start screen plus "where to start"
|
||||
controls. */
|
||||
openclaw-new-session-page {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.new-session-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
padding: clamp(24px, 10vh, 96px) 20px 32px;
|
||||
flex-direction: column;
|
||||
flex: 1 1 0;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.new-session-page__inner {
|
||||
/* Mirrors .chat-thread: the top inset doubles as the native macOS titlebar
|
||||
drag band (see the scroll container's mousedown handler). */
|
||||
.new-session-page__scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: min(720px, 100%);
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: clamp(44px, 5vh, 52px) clamp(6px, 1vw, 12px) 6px;
|
||||
}
|
||||
|
||||
/* The draft page opens with the same hero as the chat welcome screen. The
|
||||
shared .agent-chat__welcome sizing is built for a full-height chat pane,
|
||||
so neutralize its stretch/centering inside this column. */
|
||||
/* The shared welcome stretches (flex: 1) and centers via justify-content,
|
||||
which clips overflow above the scroll origin once the draft content is
|
||||
taller than the pane (short window, open folder browser). Content-size it
|
||||
here; its margin: auto still centers whenever there is spare room. */
|
||||
.new-session-page .agent-chat__welcome {
|
||||
flex: initial;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0 0 8px;
|
||||
flex: 0 0 auto;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.new-session-page .agent-chat__recents,
|
||||
.new-session-page .agent-chat__suggestions {
|
||||
align-self: center;
|
||||
margin-top: 4px;
|
||||
/* Draft block inside the centered welcome column; the welcome centers text,
|
||||
so reset alignment for the controls and composer. */
|
||||
.new-session-page__draft {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Cursor-style quiet target row: plain muted text controls with the native
|
||||
select chevrons, no chip borders; hover/focus restores full contrast. */
|
||||
.new-session-page__targets {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
gap: 4px 16px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.new-session-page__target {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--bg-elevated) 70%, transparent);
|
||||
gap: 5px;
|
||||
padding: 2px 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.new-session-page__target:hover,
|
||||
.new-session-page__target:focus-within {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@@ -54,19 +80,32 @@
|
||||
.new-session-page__target input[type="text"] {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
color: inherit;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.new-session-page__target input[type="text"] {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.new-session-page__target input[type="text"]::placeholder {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Compact by default so the browse button hugs the path; grows only as far
|
||||
as a long path needs, then ellipsizes via the input's own scroll. */
|
||||
.new-session-page__target--folder {
|
||||
flex: 1 1 220px;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.new-session-page__target--folder input {
|
||||
flex: 1 1 auto;
|
||||
width: 150px;
|
||||
max-width: 40vw;
|
||||
min-width: 0;
|
||||
field-sizing: content;
|
||||
}
|
||||
|
||||
.new-session-page__target-icon {
|
||||
@@ -197,13 +236,6 @@
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* Larger draft box than the chat composer default; wins over the shared
|
||||
.agent-chat__composer-combobox > textarea sizing by specificity. */
|
||||
.new-session-page__composer textarea.new-session-page__message {
|
||||
min-height: 112px;
|
||||
max-height: 40vh;
|
||||
}
|
||||
|
||||
.new-session-page__browser-use {
|
||||
padding: 6px 14px;
|
||||
border: 1px solid transparent;
|
||||
|
||||
Reference in New Issue
Block a user