fix(ui): isolate new-session composer lifecycle

This commit is contained in:
Jesse Merhi
2026-08-17 21:24:40 +10:00
parent 2c01bc8521
commit 4db414463d
16 changed files with 239 additions and 73 deletions
-1
View File
@@ -4286,7 +4286,6 @@ ui/src/pages/model-providers/model-providers-page.ts 1
ui/src/pages/model-providers/view.ts 3
ui/src/pages/model-setup/provider-picker.ts 3
ui/src/pages/model-setup/view.ts 2
ui/src/pages/new-session/composer.ts 1
ui/src/pages/new-session/detail-chip.ts 4
ui/src/pages/new-session/discovery.ts 4
ui/src/pages/new-session/draft-place-browser.ts 4
@@ -128,7 +128,7 @@ suite.define(() => {
];
const gateway = await installMockGateway(page, {
methodResponses: {
"commands.list": { commands },
"chat.metadata": { commands, models: [] },
},
});
await page.goto(`${suite.server.baseUrl}new`);
@@ -138,7 +138,6 @@ suite.define(() => {
await composer.fill("/");
const slashPicker = page.getByRole("listbox", { name: "Slash commands" });
await slashPicker.waitFor({ state: "visible" });
await expect.poll(() => gateway.getRequests("commands.list")).toHaveLength(1);
await expect
.poll(() => slashPicker.getByRole("option", { name: /\/status/u }).count())
.toBe(1);
+9 -6
View File
@@ -517,14 +517,15 @@ function getSlashCommandRelevance(command: SlashCommandDef, filter: string): num
export function getSlashCommandCompletions(
filter: string,
options?: { showAll?: boolean },
catalog: readonly SlashCommandDef[] = SLASH_COMMANDS,
): SlashCommandDef[] {
const lower = normalizeLowercaseStringOrEmpty(filter);
const showAll = options?.showAll ?? false;
let commands = lower
? SLASH_COMMANDS.filter(
? catalog.filter(
(command) => getSlashCommandRelevance(command, lower) < NON_MATCHING_COMMAND_RANK,
)
: SLASH_COMMANDS;
: catalog;
// When no filter text and not explicitly showing all, hide "power" tier commands
if (!lower && !showAll) {
@@ -556,12 +557,14 @@ export function getSkillDisplayName(command: SlashCommandDef): string {
return command.skillDisplayName?.trim() || command.name;
}
export function getSkillCommandCompletions(filter: string): SlashCommandDef[] {
export function getSkillCommandCompletions(
filter: string,
catalog: readonly SlashCommandDef[] = SLASH_COMMANDS,
): SlashCommandDef[] {
const lower = normalizeLowercaseStringOrEmpty(filter);
const normalized = lower.replace(/[\s_]+/gu, "-");
return SLASH_COMMANDS.filter(
(command) => command.source === "skill" && command.skillModelVisible === true,
)
return catalog
.filter((command) => command.source === "skill" && command.skillModelVisible === true)
.filter((command) => {
const displayName = normalizeLowercaseStringOrEmpty(getSkillDisplayName(command));
const displayLookup = displayName.replace(/[\s_]+/gu, "-");
+23 -7
View File
@@ -260,16 +260,32 @@ function loadRemoteSlashCommands(
return inFlight;
}
export function activateSlashCommands(params: {
export function readSlashCommandCatalog(params: {
client: GatewayBrowserClient | null;
agentId?: string | null;
}): void {
}): readonly SlashCommandDef[] {
if (!params.client) {
return buildFallbackSlashCommands();
}
const agentId = params.agentId?.trim();
const cached = params.client
? getRemoteSlashCommandCache(params.client).get(remoteSlashCommandCacheKey(agentId))
: undefined;
refreshSeq += 1;
replaceSlashCommands(cached?.commands ?? buildFallbackSlashCommands());
const metadata = peekChatMetadata(params.client, agentId);
if (Array.isArray(metadata?.commands)) {
return buildSlashCommandsFromEntries(getRemoteCommandEntries(metadata));
}
return (
getRemoteSlashCommandCache(params.client).get(remoteSlashCommandCacheKey(agentId))?.commands ??
buildFallbackSlashCommands()
);
}
export async function loadSlashCommandCatalog(params: {
client: GatewayBrowserClient | null;
agentId?: string | null;
}): Promise<readonly SlashCommandDef[]> {
if (!params.client) {
return buildFallbackSlashCommands();
}
return await loadRemoteSlashCommands(params.client, params.agentId?.trim());
}
export function applyRemoteSlashCommandsResult(params: {
@@ -4,8 +4,11 @@ import { i18n } from "../../i18n/index.ts";
import { renderChatComposer, resetChatComposerState } from "./components/chat-composer.ts";
type ComposerProps = Parameters<typeof renderChatComposer>[0];
type SessionComposerProps = Extract<ComposerProps, { style?: "session" }>;
export function createComposerProps(overrides: Partial<ComposerProps> = {}): ComposerProps {
export function createComposerProps(
overrides: Partial<SessionComposerProps> = {},
): SessionComposerProps {
return {
paneId: crypto.randomUUID(),
sessionKey: "main",
@@ -27,7 +30,7 @@ export function createComposerProps(overrides: Partial<ComposerProps> = {}): Com
};
}
export function renderComposerFixture(overrides: Partial<ComposerProps> = {}) {
export function renderComposerFixture(overrides: Partial<SessionComposerProps> = {}) {
const container = document.createElement("div");
const props = createComposerProps(overrides);
render(renderChatComposer(props), container);
@@ -11,13 +11,15 @@ export function syncChatComposerCompletionOwner(params: {
}): void {
const { draftKey, props, requestUpdate, state } = params;
const previousDraftKey = state.completionDraftKey;
const ownerChanged = previousDraftKey !== draftKey;
const gatewayClient = props.gatewayClient ?? null;
const ownerChanged =
previousDraftKey !== draftKey || state.completionGatewayClient !== gatewayClient;
state.completionDraftKey = draftKey;
state.completionGatewayClient = gatewayClient;
if (!ownerChanged) {
return;
}
props.onCompletionOwnerChange?.();
if (previousDraftKey === null) {
return;
}
@@ -25,7 +27,10 @@ export function syncChatComposerCompletionOwner(params: {
resetSkillMenuState(state);
queueMicrotask(() => {
const currentState = getChatComposerState(props.paneId);
if (currentState.completionDraftKey !== draftKey) {
if (
currentState.completionDraftKey !== draftKey ||
currentState.completionGatewayClient !== gatewayClient
) {
return;
}
const currentDraft = props.getDraft?.() ?? props.draft;
@@ -141,7 +141,7 @@ export function updateSkillMenu(
state.skillCommandRefreshTargetStart = target.start;
requestSkillCommandRefresh(props, requestUpdate, getCurrentValue, getCurrentCaret);
}
const items = getSkillCommandCompletions(target.query).filter(
const items = getSkillCommandCompletions(target.query, props.getCommandCatalog?.()).filter(
(command) => props.commandFilter?.(command) ?? true,
);
state.skillMenuTarget = target;
@@ -90,7 +90,8 @@ export function updateSlashMenu(
closeSlashMenuIfNeeded(state, requestUpdate);
return;
}
const cmd = SLASH_COMMANDS.find(
const commandCatalog = props.getCommandCatalog?.() ?? SLASH_COMMANDS;
const cmd = commandCatalog.find(
(entry) => entry.name === cmdName && (props.commandFilter?.(entry) ?? true),
);
if (cmd?.argOptions?.length) {
@@ -117,9 +118,11 @@ export function updateSlashMenu(
if (!opts.skipSlashIntent) {
requestSlashCommandRefresh(value, props, requestUpdate, getCurrentValue);
}
const items = getSlashCommandCompletions(match[1] ?? "", { showAll: true }).filter(
(command) => props.commandFilter?.(command) ?? true,
);
const items = getSlashCommandCompletions(
match[1] ?? "",
{ showAll: true },
props.getCommandCatalog?.() ?? SLASH_COMMANDS,
).filter((command) => props.commandFilter?.(command) ?? true);
state.slashMenuItems = items;
state.slashMenuOpen = items.length > 0;
state.slashMenuIndex = 0;
@@ -49,6 +49,7 @@ function createChatComposerState(): ChatComposerState {
dictation: null,
dictationDraftKey: null,
completionDraftKey: null,
completionGatewayClient: null,
dictationSelection: null,
};
}
@@ -158,6 +159,13 @@ export function releaseMicrophoneDeviceWatch(state: ChatComposerState) {
state.microphoneDeviceWatch = null;
}
function invalidatePendingCompletionRefreshes(state: ChatComposerState): void {
state.slashCommandRefreshGeneration += 1;
state.slashCommandRefreshPending = false;
state.skillCommandRefreshGeneration += 1;
state.skillCommandRefreshPending = false;
}
export function resetChatComposerState(paneId?: string) {
if (paneId) {
// Goal elapsed timers are keyed by element and cleaned up when their
@@ -165,6 +173,7 @@ export function resetChatComposerState(paneId?: string) {
const paneState = composerStates.get(paneId);
paneState?.dictation?.dispose();
if (paneState) {
invalidatePendingCompletionRefreshes(paneState);
if (paneState.composerInput) {
disconnectComposerPopoverAnchorObserver(paneState.composerInput);
}
@@ -178,6 +187,7 @@ export function resetChatComposerState(paneId?: string) {
}
for (const state of composerStates.values()) {
state.dictation?.dispose();
invalidatePendingCompletionRefreshes(state);
if (state.composerInput) {
disconnectComposerPopoverAnchorObserver(state.composerInput);
}
@@ -62,9 +62,7 @@ type ChatComposerDisabledBannerContent = {
export type ChatComposerDisabledBanner = ChatComposerDisabledBannerContent &
({ kind: "above-composer" } | { kind: "composer-replacement" });
export type ChatComposerProps = ChatAttachmentControlsProps & {
/** Lightweight first-turn rendering omits controls that require an existing session. */
style?: "session" | "new-session";
type ChatComposerCommonProps = ChatAttachmentControlsProps & {
shellClass?: string;
textareaClass?: string;
placeholder?: string;
@@ -125,8 +123,8 @@ export type ChatComposerProps = ChatAttachmentControlsProps & {
typingActors?: readonly { id: string; label: string }[];
onTypingChange?: (typing: boolean) => void;
composerControls?: TemplateResult | typeof nothing;
getCommandCatalog?: () => readonly SlashCommandDef[];
onDraftChange: (next: string) => void;
onCompletionOwnerChange?: () => void;
onHistoryKeydown?: (input: ChatInputHistoryKeyInput) => ChatInputHistoryKeyResult;
onSlashIntent?: () => void | Promise<void>;
/** Route-owned Enter semantics that run before ordinary message submission. */
@@ -139,7 +137,6 @@ export type ChatComposerProps = ChatAttachmentControlsProps & {
onDismissRealtimeTalkError?: () => void;
onDictationError?: (message: string) => void;
onAbort?: () => void;
onQueueRemove?: (id: string) => void;
onQueueRetry?: (id: string) => void;
onQueueSteer?: (id: string) => void;
onQueueMove?: (id: string, toIndex: number) => void;
@@ -151,6 +148,20 @@ export type ChatComposerProps = ChatAttachmentControlsProps & {
onGatewayQuestionSkip?: (id: string) => void | Promise<void>;
};
type ChatComposerModeProps =
| {
/** Existing-session rendering includes queue, run, reply, and context controls. */
style?: "session";
onQueueRemove: (id: string) => void;
}
| {
/** Lightweight first-turn rendering omits controls that require an existing session. */
style: "new-session";
onQueueRemove?: never;
};
export type ChatComposerProps = ChatComposerCommonProps & ChatComposerModeProps;
type PendingClearedSubmittedDraft = {
key: string;
value: string;
@@ -210,5 +221,6 @@ export type ChatComposerState = {
dictation: ComposerDictationController | null;
dictationDraftKey: string | null;
completionDraftKey: string | null;
completionGatewayClient: GatewayBrowserClient | null;
dictationSelection: { start: number; end: number } | null;
};
@@ -170,7 +170,7 @@ export function renderChatComposerView(context: ChatComposerViewContext) {
onQueueMove: props.onQueueMove,
onQueueEdit: props.queuedEdit?.onEdit,
editingId: props.queuedEdit?.editingId ?? null,
onQueueRemove: props.onQueueRemove ?? (() => undefined),
onQueueRemove: props.onQueueRemove,
})
: nothing}
${isSessionStyle && props.runError
+137 -15
View File
@@ -5,7 +5,12 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import type { CommandsListResult } from "../../../../packages/gateway-protocol/src/index.js";
import type { GatewayBrowserClient } from "../../api/gateway.ts";
import type { ApplicationContext } from "../../app/context.ts";
import { buildFallbackSlashCommands, replaceSlashCommands } from "../../lib/chat/commands.ts";
import { rememberChatMetadata } from "../../lib/chat/chat-metadata-store.ts";
import {
buildFallbackSlashCommands,
replaceSlashCommands,
SLASH_COMMANDS,
} from "../../lib/chat/commands.ts";
import { waitForFast } from "../../test-helpers/wait-for.ts";
import { adjustTextareaHeight } from "../chat/components/chat-composer-dom.ts";
import { resetChatComposerState } from "../chat/components/chat-composer.ts";
@@ -92,12 +97,29 @@ afterEach(() => {
});
describe("new-session composer prompt authoring", () => {
it("shares slash and skill completion while omitting existing-session actions", () => {
it("shares slash and skill completion while omitting existing-session actions", async () => {
const container = document.createElement("div");
const attachmentDraft = new NewSessionAttachmentDraft(() => draw());
const onSubmit = vi.fn();
let message = "";
attachmentDrafts.push(attachmentDraft);
const client = { request: vi.fn() } as unknown as GatewayBrowserClient;
rememberChatMetadata(client, "main", {
commands: [
{
name: "prose",
textAliases: ["/prose"],
description: "Draft polished prose.",
source: "skill",
scope: "text",
acceptsArgs: true,
skillModelVisible: true,
},
],
});
const context = {
gateway: { snapshot: { client, phase: "connected" } },
} as unknown as ApplicationContext;
const draw = () => {
render(
@@ -106,7 +128,7 @@ describe("new-session composer prompt authoring", () => {
getCurrentAgentId: () => "main",
attachmentDraft,
canSubmit: true,
context: undefined,
context,
isCatalogTarget: true,
message,
modelControl: new NewSessionModelControl(draw),
@@ -132,17 +154,6 @@ describe("new-session composer prompt authoring", () => {
textarea.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText" }));
};
draw();
replaceSlashCommands([
...buildFallbackSlashCommands(),
{
key: "prose",
name: "prose",
description: "Draft polished prose.",
source: "skill",
skillModelVisible: true,
},
]);
const shell = container.querySelector<HTMLElement>(".agent-chat__composer-shell");
expect(shell?.dataset.composerStyle).toBe("new-session");
expect(container.querySelector(".agent-chat__composer-status-stack")).toBeNull();
@@ -161,7 +172,9 @@ describe("new-session composer prompt authoring", () => {
input("Polish this with $pro");
const skillMenu = container.querySelector<HTMLElement>("[role='listbox']");
expect(skillMenu?.getAttribute("aria-label")).toBe("Skill references");
expect(skillMenu?.querySelector(".slash-menu-name")?.textContent).toBe("prose");
await waitForFast(() =>
expect(skillMenu?.querySelector(".slash-menu-name")?.textContent).toBe("prose"),
);
const textarea = container.querySelector<HTMLTextAreaElement>("textarea");
textarea?.dispatchEvent(
@@ -277,9 +290,118 @@ describe("new-session composer prompt authoring", () => {
expect(names).not.toContain("/agent-a-only");
expect(names).toContain("/agent-b-only");
});
expect(SLASH_COMMANDS.map((entry) => entry.name)).toContain("previous-agent-only");
expect(SLASH_COMMANDS.map((entry) => entry.name)).not.toContain("agent-a-only");
expect(SLASH_COMMANDS.map((entry) => entry.name)).not.toContain("agent-b-only");
expect(message).toBe("/");
expect(onSubmit).not.toHaveBeenCalled();
});
it.each([
{ kind: "slash", draft: "/", resetBeforeSwitch: false },
{ kind: "skill", draft: "Use $", resetBeforeSwitch: false },
{ kind: "slash", draft: "/", resetBeforeSwitch: true },
{ kind: "skill", draft: "Use $", resetBeforeSwitch: true },
] as const)(
"fences delayed $kind completion across a Gateway client replacement (reset=$resetBeforeSwitch)",
async ({ kind, draft, resetBeforeSwitch }) => {
const remoteCommand = (owner: "a" | "b") =>
kind === "skill"
? {
name: `${owner}-skill`,
textAliases: [`/${owner}-skill`],
description: `Only available from client ${owner.toUpperCase()}.`,
source: "skill" as const,
scope: "text" as const,
acceptsArgs: true,
skillModelVisible: true,
}
: {
name: `${owner}-only`,
textAliases: [`/${owner}-only`],
description: `Only available from client ${owner.toUpperCase()}.`,
source: "plugin" as const,
scope: "text" as const,
acceptsArgs: false,
};
let resolveClientA: (value: CommandsListResult) => void = () => undefined;
const clientAResult = new Promise<CommandsListResult>((resolve) => {
resolveClientA = resolve;
});
const requestA = vi.fn(async () => await clientAResult);
const requestB = vi.fn(async () => ({ commands: [remoteCommand("b")] }));
const clientA = { request: requestA } as unknown as GatewayBrowserClient;
const clientB = { request: requestB } as unknown as GatewayBrowserClient;
let activeClient = clientA;
const container = document.createElement("div");
const attachmentDraft = new NewSessionAttachmentDraft(() => draw());
attachmentDrafts.push(attachmentDraft);
let message = "";
const draw = () => {
const context = {
gateway: { snapshot: { client: activeClient, phase: "connected" } },
} as unknown as ApplicationContext;
render(
renderNewSessionDraftComposer({
agentId: "main",
getCurrentAgentId: () => "main",
attachmentDraft,
canSubmit: true,
context,
isCatalogTarget: true,
message,
modelControl: new NewSessionModelControl(draw),
requiresModifier: false,
submitting: false,
onInput: (next) => {
message = next;
draw();
},
onRequestUpdate: draw,
onSubmit: () => undefined,
}),
container,
);
};
const input = () => {
const textarea = container.querySelector<HTMLTextAreaElement>("textarea");
if (!textarea) {
throw new Error("Expected composer textarea");
}
textarea.value = draft;
textarea.setSelectionRange(draft.length, draft.length);
textarea.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText" }));
};
const menuNames = () =>
Array.from(container.querySelectorAll<HTMLElement>(".slash-menu-name")).map((entry) =>
entry.textContent?.trim(),
);
draw();
input();
await waitForFast(() => expect(requestA).toHaveBeenCalledOnce());
if (resetBeforeSwitch) {
resetChatComposerState("new-session");
}
activeClient = clientB;
draw();
input();
await waitForFast(() => expect(requestB).toHaveBeenCalledOnce());
await waitForFast(() =>
expect(menuNames()).toContain(kind === "skill" ? "b-skill" : "/b-only"),
);
resolveClientA({ commands: [remoteCommand("a")] });
await clientAResult;
await Promise.resolve();
await Promise.resolve();
expect(menuNames()).not.toContain(kind === "skill" ? "a-skill" : "/a-only");
expect(menuNames()).toContain(kind === "skill" ? "b-skill" : "/b-only");
},
);
});
describe("new-session composer keyboard submission", () => {
+7 -14
View File
@@ -5,7 +5,7 @@ import { t } from "../../i18n/index.ts";
import "../../components/tooltip.ts";
import type { ChatAttachment } from "../../lib/chat/chat-types.ts";
import { formatUiError } from "../../lib/format-error.ts";
import { activateSlashCommands, refreshSlashCommands } from "../chat/chat-commands.ts";
import { loadSlashCommandCatalog, readSlashCommandCatalog } from "../chat/chat-commands.ts";
import { renderChatAttachmentMenu } from "../chat/components/chat-attachments.ts";
import { renderChatComposer } from "../chat/components/chat-composer.ts";
import type { NewSessionAttachmentDraft } from "./attachment-draft.ts";
@@ -188,6 +188,7 @@ function renderNewSessionComposer(options: NewSessionComposerOptions) {
paneId: "new-session",
sessionKey: "new-session",
currentAgentId: options.agentId,
gatewayClient: client,
connected: context ? context.gateway.snapshot.phase === "connected" : true,
canSend: canCompose,
canSubmit: options.canSubmit,
@@ -209,26 +210,18 @@ function renderNewSessionComposer(options: NewSessionComposerOptions) {
onPendingReadsChange: options.onPendingReadsChange,
onOpenImage: options.onOpenImage,
composerControls,
getCommandCatalog: () =>
readSlashCommandCatalog({ client, agentId: options.getCurrentAgentId() }),
getDraft: () => options.message,
onDraftChange: options.onInput,
onRequestUpdate: options.onRequestUpdate,
onCompletionOwnerChange: () =>
activateSlashCommands({ client, agentId: options.getCurrentAgentId() }),
onSlashIntent: client
? () => {
const agentId = options.agentId;
return refreshSlashCommands({
const agentId = options.getCurrentAgentId();
return loadSlashCommandCatalog({
client,
agentId,
shouldApply: () => {
const snapshot = options.context?.gateway.snapshot;
return (
snapshot?.phase === "connected" &&
snapshot.client === client &&
options.getCurrentAgentId() === agentId
);
},
});
}).then(() => undefined);
}
: undefined,
onSubmitShortcut: () => {
@@ -19,6 +19,7 @@ import { isTerminalAvailable } from "../../lib/terminal-availability.ts";
import { createManagedWorktree } from "../../lib/worktrees/create-worktree.ts";
import { buildChatApiAttachments, restoreChatApiAttachments } from "../chat/attachment-api.ts";
import { requiresChatModelSetup } from "../chat/chat-model-setup.ts";
import { resetChatComposerState } from "../chat/components/chat-composer-state.ts";
import { prepareInitialUserMessageHandoff } from "../chat/initial-turn-handoff.ts";
import { NewSessionAttachmentDraft } from "./attachment-draft.ts";
import * as catalog from "./catalog-target.ts";
@@ -302,6 +303,7 @@ export class DraftSubmissionFlow {
}
resetDraft() {
resetChatComposerState("new-session");
const preservePendingCloud = Boolean(this.pendingCloud.sessionKey);
this.blockedSubmitGate = null;
this.invalidate();
@@ -667,6 +669,7 @@ export class DraftSubmissionFlow {
}
disconnect() {
resetChatComposerState("new-session");
this.attachmentDraft.reset({ release: true });
}
@@ -123,7 +123,7 @@ describe("new session draft route ownership", () => {
expect(page.querySelector('[role="listbox"][aria-label="Slash commands"]')).toBeNull();
});
it("retires the open command menu through the actual agent selector", async () => {
it("keeps active-chat commands out through the actual agent selector", async () => {
const page = document.createElement("openclaw-new-session-page") as NewSessionElement;
page.place.agents = () =>
[
@@ -141,8 +141,13 @@ describe("new session draft route ownership", () => {
source: "plugin",
},
]);
await enterMessage(page, "/agent-a");
expect(page.querySelector(".slash-menu-name")?.textContent?.trim()).toBe("/agent-a-only");
await enterMessage(page, "/");
expect(page.querySelector('[role="listbox"][aria-label="Slash commands"]')).not.toBeNull();
expect(
Array.from(page.querySelectorAll<HTMLElement>(".slash-menu-name")).map((entry) =>
entry.textContent?.trim(),
),
).not.toContain("/agent-a-only");
const selector = page.querySelector<HTMLElement & { onSelect: (agentId: string) => void }>(
"openclaw-agent-select",
+2 -9
View File
@@ -18,7 +18,6 @@ import { OpenClawLightDomElement } from "../../lit/openclaw-element.ts";
import { SubscriptionsController } from "../../lit/subscriptions-controller.ts";
import "../../styles/chat.css";
import "../../styles/new-session.css";
import { resetChatComposerState } from "../chat/components/chat-composer.ts";
import { renderChatImageLightbox } from "../chat/components/chat-image-lightbox.ts";
import { renderWelcomeState } from "../chat/components/chat-welcome.ts";
import * as catalog from "./catalog-target.ts";
@@ -237,7 +236,6 @@ class NewSessionPage extends OpenClawLightDomElement {
this.browser.disconnect();
this.submission.disconnect();
this.closeConnectMachine();
resetChatComposerState("new-session");
super.disconnectedCallback();
}
@@ -310,7 +308,6 @@ class NewSessionPage extends OpenClawLightDomElement {
}
private resetDraft() {
resetChatComposerState("new-session");
this.place.resetDraft();
this.submission.resetDraft();
this.messageOwnerKey = catalog.routeKey(this.data);
@@ -324,13 +321,9 @@ class NewSessionPage extends OpenClawLightDomElement {
});
}
private setMessage(message: string, ownerKey = catalog.routeKey(this.data)) {
this.submission.setMessage(message);
this.messageOwnerKey = ownerKey;
}
private setMessageFromUser(message: string) {
this.setMessage(message, catalog.routeKeyFromSearch(window.location.search));
this.submission.setMessage(message);
this.messageOwnerKey = catalog.routeKeyFromSearch(window.location.search);
}
private renderAgentSelect() {