mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(control-ui): autocomplete model thinking levels (#123507)
* fix(control-ui): autocomplete model thinking levels * fix(control-ui): keep thinking options model-current * fix(control-ui): execute catalog thinking levels * fix(control-ui): close stale think picker --------- Co-authored-by: Jesse Merhi <openclaw@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
// Control UI E2E proves model-aware /think completion in the rendered composer.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
const suite = createControlUiE2eSuite({
|
||||
name: "Control UI thinking argument completion",
|
||||
});
|
||||
|
||||
const VIEWPORTS = [
|
||||
{ name: "mobile", width: 390, height: 844 },
|
||||
{ name: "tablet", width: 768, height: 1024 },
|
||||
{ name: "desktop", width: 1440, height: 900 },
|
||||
] as const;
|
||||
|
||||
suite.define(() => {
|
||||
it.each(VIEWPORTS)(
|
||||
"opens the active model's thinking levels above the composer ($name)",
|
||||
async (viewport) => {
|
||||
await suite.withPage({ viewport }, async ({ page }) => {
|
||||
const browserErrors: string[] = [];
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") {
|
||||
browserErrors.push(message.text());
|
||||
}
|
||||
});
|
||||
page.on("pageerror", (error) => browserErrors.push(error.message));
|
||||
|
||||
const gateway = await installMockGateway(page, {
|
||||
models: [
|
||||
{
|
||||
id: "gpt-5.6-sol",
|
||||
name: "GPT-5.6 Sol",
|
||||
provider: "openai",
|
||||
thinkingLevels: [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "minimal", label: "minimal" },
|
||||
{ id: "low", label: "low" },
|
||||
{ id: "medium", label: "medium" },
|
||||
{ id: "high", label: "high" },
|
||||
{ id: "xhigh", label: "xhigh" },
|
||||
{ id: "max", label: "max" },
|
||||
{ id: "ultra", label: "ultra" },
|
||||
],
|
||||
},
|
||||
],
|
||||
methodResponses: {
|
||||
"sessions.list": {
|
||||
count: 1,
|
||||
defaults: {
|
||||
contextTokens: 200_000,
|
||||
model: "gpt-5.6-sol",
|
||||
modelProvider: "openai",
|
||||
},
|
||||
path: "",
|
||||
sessions: [
|
||||
{
|
||||
key: "main",
|
||||
kind: "direct",
|
||||
model: "gpt-5.6-sol",
|
||||
modelProvider: "openai",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
],
|
||||
ts: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto(`${suite.server.baseUrl}chat`);
|
||||
await gateway.waitForRequest("chat.startup");
|
||||
const composer = page.locator(".agent-chat__composer-combobox textarea");
|
||||
await composer.waitFor({ state: "visible" });
|
||||
await expect.poll(() => composer.isEnabled()).toBe(true);
|
||||
|
||||
await composer.fill("/think");
|
||||
await composer.press("Tab");
|
||||
|
||||
const picker = page.locator(".slash-menu[role='listbox']");
|
||||
await picker.waitFor({ state: "visible" });
|
||||
await expect.poll(() => composer.inputValue()).toBe("/think ");
|
||||
await expect
|
||||
.poll(() => picker.getByRole("option").locator(".slash-menu-name").allTextContents())
|
||||
.toEqual(["default", "off", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
||||
|
||||
const [pickerBox, inputBox] = await Promise.all([
|
||||
picker.boundingBox(),
|
||||
page.locator(".agent-chat__input").boundingBox(),
|
||||
]);
|
||||
expect(pickerBox).not.toBeNull();
|
||||
expect(inputBox).not.toBeNull();
|
||||
expect((pickerBox?.y ?? 0) + (pickerBox?.height ?? 0)).toBeLessThanOrEqual(
|
||||
(inputBox?.y ?? 0) + 1,
|
||||
);
|
||||
expect(pickerBox?.x ?? -1).toBeGreaterThanOrEqual(0);
|
||||
expect((pickerBox?.x ?? 0) + (pickerBox?.width ?? 0)).toBeLessThanOrEqual(viewport.width);
|
||||
expect(pickerBox?.y ?? -1).toBeGreaterThanOrEqual(0);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(
|
||||
viewport.width,
|
||||
);
|
||||
expect(browserErrors).toEqual([]);
|
||||
|
||||
await composer.press("ArrowUp");
|
||||
await composer.press("Tab");
|
||||
await expect.poll(() => composer.inputValue()).toBe("/think ultra");
|
||||
await composer.press("Enter");
|
||||
const patchRequest = await gateway.waitForRequest("sessions.patch");
|
||||
expect(patchRequest.params).toMatchObject({
|
||||
key: "main",
|
||||
thinkingLevel: "ultra",
|
||||
});
|
||||
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
if (artifactDir) {
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
path: path.join(artifactDir, `think-arguments-${viewport.name}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -43,16 +43,39 @@ export type ChatThinkingSelectState = {
|
||||
function resolveThinkingLevelOptionsForSession(
|
||||
session: ChatThinkingTarget | undefined,
|
||||
defaults: ThinkingSessionDefaults,
|
||||
catalog: readonly ModelCatalogEntry[] = [],
|
||||
fallbackLabels?: readonly string[],
|
||||
): GatewayThinkingLevelOption[] {
|
||||
const { provider, model } = resolveThinkingTargetModel({ defaults, session });
|
||||
return resolveThinkingLevelOptions({ catalog: [], defaults, model, provider, session });
|
||||
return resolveThinkingLevelOptions({
|
||||
catalog,
|
||||
defaults,
|
||||
fallbackLabels,
|
||||
model,
|
||||
provider,
|
||||
session,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveThinkingCommandArgOptionsForSession(
|
||||
session: ChatThinkingTarget | undefined,
|
||||
defaults?: SessionsListResult["defaults"],
|
||||
catalog: readonly ModelCatalogEntry[] = [],
|
||||
): string[] {
|
||||
const options = resolveThinkingLevelOptionsForSession(session, defaults, catalog, []).map(
|
||||
(level) => normalizeThinkingOptionValue(level.id),
|
||||
);
|
||||
return options.length > 0
|
||||
? ["default", ...new Set(options.filter((option) => option && option !== "default"))]
|
||||
: [];
|
||||
}
|
||||
|
||||
export function formatThinkingCommandOptionsForSession(
|
||||
session: ChatThinkingTarget | undefined,
|
||||
defaults?: SessionsListResult["defaults"],
|
||||
catalog: readonly ModelCatalogEntry[] = [],
|
||||
): string {
|
||||
const options = resolveThinkingLevelOptionsForSession(session, defaults)
|
||||
const options = resolveThinkingLevelOptionsForSession(session, defaults, catalog)
|
||||
.map((level) => level.label)
|
||||
.join(", ");
|
||||
return options.split(", ").includes("default") ? options : `default, ${options}`;
|
||||
@@ -62,13 +85,14 @@ export function resolveThinkingLevelInput(
|
||||
rawLevel: string,
|
||||
session: ChatThinkingTarget | undefined,
|
||||
defaults: ThinkingSessionDefaults,
|
||||
catalog: readonly ModelCatalogEntry[] = [],
|
||||
): string | undefined {
|
||||
const normalized = normalizeThinkLevel(rawLevel);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
const rawKey = normalizeLowercaseStringOrEmpty(rawLevel);
|
||||
return resolveThinkingLevelOptionsForSession(session, defaults)
|
||||
return resolveThinkingLevelOptionsForSession(session, defaults, catalog)
|
||||
.map((option) => ({
|
||||
id: normalizeThinkLevel(option.id) ?? normalizeLowercaseStringOrEmpty(option.id),
|
||||
label: normalizeLowercaseStringOrEmpty(option.label),
|
||||
@@ -80,8 +104,9 @@ export function isThinkingLevelOptionForSession(
|
||||
session: ChatThinkingTarget | undefined,
|
||||
defaults: ThinkingSessionDefaults,
|
||||
level: string,
|
||||
catalog: readonly ModelCatalogEntry[] = [],
|
||||
): boolean {
|
||||
return resolveThinkingLevelOptionsForSession(session, defaults).some((option) => {
|
||||
return resolveThinkingLevelOptionsForSession(session, defaults, catalog).some((option) => {
|
||||
const id = normalizeThinkLevel(option.id) ?? normalizeLowercaseStringOrEmpty(option.id);
|
||||
return id === level || normalizeThinkLevel(option.label) === level;
|
||||
});
|
||||
@@ -167,6 +192,7 @@ function resolveThinkingCatalogEntry(
|
||||
function resolveThinkingLevelOptions(params: {
|
||||
catalog: readonly ModelCatalogEntry[];
|
||||
defaults: ThinkingSessionDefaults;
|
||||
fallbackLabels?: readonly string[];
|
||||
hideUnsupportedOffOnly?: boolean;
|
||||
model: string | null;
|
||||
provider: string | null;
|
||||
@@ -202,7 +228,7 @@ function resolveThinkingLevelOptions(params: {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const labels = explicitLabels ?? BASE_THINKING_LEVELS;
|
||||
const labels = explicitLabels ?? params.fallbackLabels ?? BASE_THINKING_LEVELS;
|
||||
return labels.map((label) => ({
|
||||
id: normalizeThinkLevel(label) ?? normalizeLowercaseStringOrEmpty(label),
|
||||
label,
|
||||
|
||||
@@ -1099,6 +1099,50 @@ describe("executeSlashCommand directives", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts a thinking level advertised only by the active model catalog", async () => {
|
||||
const request = vi.fn(async (method: string, payload?: unknown) => {
|
||||
if (method === "sessions.list") {
|
||||
return {
|
||||
sessions: [
|
||||
row("agent:main:main", {
|
||||
model: "gpt-5.6-sol",
|
||||
modelProvider: "openai",
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
if (method === "sessions.patch") {
|
||||
return { ok: true, ...((payload ?? {}) as object) };
|
||||
}
|
||||
throw new Error(`unexpected method: ${method}`);
|
||||
});
|
||||
|
||||
const result = await executeSlashCommand(
|
||||
createTestGatewayClient(request),
|
||||
"agent:main:main",
|
||||
"think",
|
||||
"ultra",
|
||||
{
|
||||
chatModelCatalog: [
|
||||
{
|
||||
id: "gpt-5.6-sol",
|
||||
name: "GPT-5.6 Sol",
|
||||
provider: "openai",
|
||||
reasoning: true,
|
||||
thinkingLevels: [{ id: "ultra", label: "ultra" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.content).toBe(t("chat.commandResults.thinking.set", { level: "**ultra**" }));
|
||||
expect(request).toHaveBeenCalledWith("sessions.patch", {
|
||||
key: "agent:main:main",
|
||||
thinkingLevel: "ultra",
|
||||
});
|
||||
expectNoRequestCall(request, "models.list");
|
||||
});
|
||||
|
||||
it("clears thinking override for /think default", async () => {
|
||||
const request = vi.fn(async (method: string, payload?: unknown) => {
|
||||
if (method === "sessions.patch") {
|
||||
|
||||
@@ -375,7 +375,7 @@ async function executeThink(
|
||||
t("chat.commandResults.thinking.current", {
|
||||
level: resolveCurrentThinkingLevel(session, defaults, models),
|
||||
}),
|
||||
formatThinkingCommandOptionsForSession(session, defaults),
|
||||
formatThinkingCommandOptionsForSession(session, defaults, models),
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
@@ -405,20 +405,21 @@ async function executeThink(
|
||||
|
||||
try {
|
||||
const { session, defaults } = await loadCurrentSessionState(context, sessionKey);
|
||||
const level = resolveThinkingLevelInput(rawLevel, session, defaults);
|
||||
const modelCatalog = context.chatModelCatalog ?? context.modelCatalog ?? [];
|
||||
const level = resolveThinkingLevelInput(rawLevel, session, defaults, modelCatalog);
|
||||
if (!level) {
|
||||
return {
|
||||
content: t("chat.commandResults.thinking.unrecognized", {
|
||||
level: rawLevel,
|
||||
options: formatThinkingCommandOptionsForSession(session, defaults),
|
||||
options: formatThinkingCommandOptionsForSession(session, defaults, modelCatalog),
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (!isThinkingLevelOptionForSession(session, defaults, level)) {
|
||||
if (!isThinkingLevelOptionForSession(session, defaults, level, modelCatalog)) {
|
||||
return {
|
||||
content: t("chat.commandResults.thinking.unsupported", {
|
||||
level: rawLevel,
|
||||
options: formatThinkingCommandOptionsForSession(session, defaults),
|
||||
options: formatThinkingCommandOptionsForSession(session, defaults, modelCatalog),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ export function createComposerProps(overrides: Partial<ComposerProps> = {}): Com
|
||||
stream: null,
|
||||
queue: [],
|
||||
draft: "",
|
||||
modelCatalog: [],
|
||||
modelSwitching: false,
|
||||
sessions: null,
|
||||
assistantName: "OpenClaw",
|
||||
onDraftChange: vi.fn(),
|
||||
|
||||
@@ -340,6 +340,8 @@ export class ChatPane extends ChatPaneLayoutRender {
|
||||
sendShortcut: state.settings.chatSendShortcut,
|
||||
followUpMode: state.chatFollowUpMode,
|
||||
draft: state.chatMessage,
|
||||
modelCatalog: state.chatModelCatalog,
|
||||
modelSwitching: Boolean(state.chatModelSwitchPromises[state.sessionKey]),
|
||||
queue: state.chatQueue,
|
||||
queuedOutboxCount: state.chatQueue.filter((item) => !item.pendingRunId).length,
|
||||
realtimeTalkActive: state.realtimeTalkActive,
|
||||
|
||||
@@ -673,6 +673,8 @@ function createChatProps(overrides: Partial<ChatProps> = {}): ChatProps {
|
||||
streamStartedAt: null,
|
||||
assistantAvatarUrl: null,
|
||||
draft: "",
|
||||
modelCatalog: [],
|
||||
modelSwitching: false,
|
||||
queue: [],
|
||||
realtimeTalkActive: false,
|
||||
realtimeTalkStatus: "idle",
|
||||
@@ -3456,22 +3458,24 @@ describe("chat slash menu accessibility", () => {
|
||||
...overrides
|
||||
}: Partial<ChatProps> = {}) {
|
||||
let draft = "";
|
||||
let currentOverrides = overrides;
|
||||
const container = document.createElement("div");
|
||||
const onDraftChange = vi.fn((next: string) => {
|
||||
draft = next;
|
||||
observeDraftChange?.(next);
|
||||
});
|
||||
const renderCurrent = () => {
|
||||
const renderCurrent = (nextOverrides: Partial<ChatProps> = {}) => {
|
||||
currentOverrides = { ...currentOverrides, ...nextOverrides };
|
||||
renderChatInto(container, {
|
||||
draft,
|
||||
getDraft: () => draft,
|
||||
onDraftChange,
|
||||
onRequestUpdate: renderCurrent,
|
||||
...overrides,
|
||||
...currentOverrides,
|
||||
});
|
||||
};
|
||||
renderCurrent();
|
||||
return { container };
|
||||
return { container, renderCurrent };
|
||||
}
|
||||
|
||||
function createSlashRerenderHarness() {
|
||||
@@ -4195,6 +4199,85 @@ describe("chat slash menu accessibility", () => {
|
||||
expect(listbox?.querySelector(`#${activeId}`)?.getAttribute("aria-selected")).toBe("true");
|
||||
});
|
||||
|
||||
it("opens model-supported thinking arguments after tab-completing /think", () => {
|
||||
const sessions = createSessionsListResult({
|
||||
model: "gpt-5.6-sol",
|
||||
modelProvider: "openai",
|
||||
});
|
||||
const session = expectDefined(sessions.sessions[0], "active session");
|
||||
session.thinkingLevels = [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "minimal", label: "minimal" },
|
||||
{ id: "low", label: "low" },
|
||||
{ id: "medium", label: "medium" },
|
||||
{ id: "high", label: "high" },
|
||||
{ id: "xhigh", label: "xhigh" },
|
||||
{ id: "max", label: "max" },
|
||||
{ id: "ultra", label: "ultra" },
|
||||
];
|
||||
const { container } = createReactiveDraftHarness({ sessions });
|
||||
|
||||
inputDraft(container, "/think");
|
||||
keydownComposer(container, "Tab");
|
||||
|
||||
expect(container.querySelector<HTMLTextAreaElement>("textarea")?.value).toBe("/think ");
|
||||
expect(
|
||||
Array.from(container.querySelectorAll<HTMLElement>(".slash-menu [role='option']")).map(
|
||||
(option) => option.querySelector(".slash-menu-name")?.textContent?.trim(),
|
||||
),
|
||||
).toEqual(["default", "off", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
||||
});
|
||||
|
||||
it("suppresses thinking arguments while the active model is switching", () => {
|
||||
const sessions = createSessionsListResult({
|
||||
model: "gpt-5.6-sol",
|
||||
modelProvider: "openai",
|
||||
});
|
||||
const session = expectDefined(sessions.sessions[0], "active session");
|
||||
session.thinkingLevels = [
|
||||
{ id: "low", label: "low" },
|
||||
{ id: "high", label: "high" },
|
||||
];
|
||||
const { container } = createReactiveDraftHarness({ modelSwitching: true, sessions });
|
||||
|
||||
inputDraft(container, "/think");
|
||||
keydownComposer(container, "Tab");
|
||||
|
||||
expect(container.querySelector<HTMLTextAreaElement>("textarea")?.value).toBe("/think ");
|
||||
expect(container.querySelector(".slash-menu")).toBeNull();
|
||||
});
|
||||
|
||||
it("closes open thinking arguments when the active model starts switching", () => {
|
||||
const sessions = createSessionsListResult({
|
||||
model: "gpt-5.6-sol",
|
||||
modelProvider: "openai",
|
||||
});
|
||||
const session = expectDefined(sessions.sessions[0], "active session");
|
||||
session.thinkingLevels = [
|
||||
{ id: "low", label: "low" },
|
||||
{ id: "high", label: "high" },
|
||||
];
|
||||
const { container, renderCurrent } = createReactiveDraftHarness({ sessions });
|
||||
|
||||
inputDraft(container, "/think");
|
||||
keydownComposer(container, "Tab");
|
||||
expect(container.querySelector(".slash-menu")).not.toBeNull();
|
||||
expect(
|
||||
container
|
||||
.querySelector<HTMLTextAreaElement>("textarea")
|
||||
?.getAttribute("aria-activedescendant"),
|
||||
).toBe("chat-single-slash-option-arg-think-default");
|
||||
|
||||
renderCurrent({ modelSwitching: true });
|
||||
|
||||
expect(container.querySelector(".slash-menu")).toBeNull();
|
||||
expect(
|
||||
container
|
||||
.querySelector<HTMLTextAreaElement>("textarea")
|
||||
?.hasAttribute("aria-activedescendant"),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("clears active descendant when suggestions close", () => {
|
||||
const harness = createSlashRerenderHarness();
|
||||
let container = harness.inputAndRender(harness.container, "/");
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
ControlUiSessionPullRequest,
|
||||
} from "../../../../src/gateway/control-ui-contract.js";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import type { GatewaySessionRow, SessionsListResult } from "../../api/types.ts";
|
||||
import type { GatewaySessionRow, ModelCatalogEntry, SessionsListResult } from "../../api/types.ts";
|
||||
import type { ExecApprovalDecision, ExecApprovalRequest } from "../../app/exec-approval.ts";
|
||||
import type { QuestionPrompt } from "../../app/question-prompt.ts";
|
||||
import type { ChatSendShortcut } from "../../app/settings.ts";
|
||||
@@ -125,6 +125,8 @@ export type ChatProps = ChatTaskSuggestionTrayProps &
|
||||
runOutputTokens?: number | null;
|
||||
assistantAvatarUrl?: string | null;
|
||||
draft: string;
|
||||
modelCatalog: readonly ModelCatalogEntry[];
|
||||
modelSwitching: boolean;
|
||||
queue: ChatQueueItem[];
|
||||
queuedOutboxCount?: number;
|
||||
realtimeTalkActive?: boolean;
|
||||
@@ -408,6 +410,8 @@ export function renderChat(props: ChatProps) {
|
||||
stream: props.stream,
|
||||
queue: props.queue,
|
||||
draft: props.draft,
|
||||
modelCatalog: props.modelCatalog,
|
||||
modelSwitching: props.modelSwitching,
|
||||
sessions: props.sessions,
|
||||
toolOverrides: props.toolOverrides,
|
||||
capabilityMenu: props.capabilityMenu,
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
type SlashCommandCategory,
|
||||
type SlashCommandDef,
|
||||
} from "../../../lib/chat/commands.ts";
|
||||
import { resolveThinkingCommandArgOptionsForSession } from "../../../lib/chat/thinking.ts";
|
||||
import { areUiSessionKeysEquivalent } from "../../../lib/sessions/session-key.ts";
|
||||
import { paneDomId } from "./chat-composer-dom.ts";
|
||||
import { commitComposerDraft, getChatComposerState } from "./chat-composer-state.ts";
|
||||
import type { ChatComposerProps, ChatComposerState } from "./chat-composer-types.ts";
|
||||
@@ -39,6 +41,26 @@ function closeSlashMenuIfNeeded(state: ChatComposerState, requestUpdate: () => v
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
function resolveSlashCommandArgOptions(
|
||||
command: SlashCommandDef,
|
||||
props: ChatComposerProps,
|
||||
): string[] {
|
||||
if (command.key !== "think") {
|
||||
return command.argOptions ?? [];
|
||||
}
|
||||
if (props.modelSwitching) {
|
||||
return [];
|
||||
}
|
||||
const session = props.sessions?.sessions.find((row) =>
|
||||
areUiSessionKeysEquivalent(row.key, props.sessionKey),
|
||||
);
|
||||
return resolveThinkingCommandArgOptionsForSession(
|
||||
session,
|
||||
props.sessions?.defaults,
|
||||
props.modelCatalog,
|
||||
);
|
||||
}
|
||||
|
||||
function requestSlashCommandRefresh(
|
||||
value: string,
|
||||
props: ChatComposerProps,
|
||||
@@ -85,10 +107,11 @@ export function updateSlashMenu(
|
||||
return;
|
||||
}
|
||||
const cmd = SLASH_COMMANDS.find((entry) => entry.name === cmdName);
|
||||
if (cmd?.argOptions?.length) {
|
||||
const argOptions = cmd ? resolveSlashCommandArgOptions(cmd, props) : [];
|
||||
if (cmd && argOptions.length > 0) {
|
||||
const filtered = argFilter
|
||||
? cmd.argOptions.filter((arg) => arg.toLowerCase().startsWith(argFilter))
|
||||
: cmd.argOptions;
|
||||
? argOptions.filter((arg) => arg.toLowerCase().startsWith(argFilter))
|
||||
: argOptions;
|
||||
if (filtered.length > 0) {
|
||||
state.slashMenuMode = "args";
|
||||
state.slashMenuCommand = cmd;
|
||||
@@ -129,11 +152,12 @@ export function selectSlashCommand(
|
||||
requestUpdate: () => void,
|
||||
) {
|
||||
const state = getChatComposerState(props.paneId);
|
||||
if (cmd.argOptions?.length) {
|
||||
const argOptions = resolveSlashCommandArgOptions(cmd, props);
|
||||
if (argOptions.length > 0) {
|
||||
commitComposerDraft(props, `/${cmd.name} `);
|
||||
state.slashMenuMode = "args";
|
||||
state.slashMenuCommand = cmd;
|
||||
state.slashMenuArgItems = cmd.argOptions;
|
||||
state.slashMenuArgItems = argOptions;
|
||||
state.slashMenuOpen = true;
|
||||
state.slashMenuIndex = 0;
|
||||
state.slashMenuItems = [];
|
||||
@@ -158,11 +182,12 @@ export function tabCompleteSlashCommand(
|
||||
requestUpdate: () => void,
|
||||
) {
|
||||
const state = getChatComposerState(props.paneId);
|
||||
if (cmd.argOptions?.length) {
|
||||
const argOptions = resolveSlashCommandArgOptions(cmd, props);
|
||||
if (argOptions.length > 0) {
|
||||
commitComposerDraft(props, `/${cmd.name} `);
|
||||
state.slashMenuMode = "args";
|
||||
state.slashMenuCommand = cmd;
|
||||
state.slashMenuArgItems = cmd.argOptions;
|
||||
state.slashMenuArgItems = argOptions;
|
||||
state.slashMenuOpen = true;
|
||||
state.slashMenuIndex = 0;
|
||||
state.slashMenuItems = [];
|
||||
@@ -367,10 +392,10 @@ export function renderSlashMenu(
|
||||
</span>
|
||||
<span class="slash-menu-trailing">
|
||||
<span class="slash-menu-desc">${getSlashCommandDescription(cmd)}</span>
|
||||
${cmd.argOptions?.length
|
||||
${resolveSlashCommandArgOptions(cmd, props).length
|
||||
? html`<span class="slash-menu-badge"
|
||||
>${t("chat.commands.optionCount", {
|
||||
count: String(cmd.argOptions.length),
|
||||
count: String(resolveSlashCommandArgOptions(cmd, props).length),
|
||||
})}</span
|
||||
>`
|
||||
: nothing}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ProgressCard } from "@openclaw/gateway-protocol";
|
||||
import type { TemplateResult, nothing } from "lit";
|
||||
import type { GatewayBrowserClient } from "../../../api/gateway.ts";
|
||||
import type { SessionsListResult } from "../../../api/types.ts";
|
||||
import type { ModelCatalogEntry, SessionsListResult } from "../../../api/types.ts";
|
||||
import type { QuestionPrompt } from "../../../app/question-prompt.ts";
|
||||
import type { ChatSendShortcut } from "../../../app/settings.ts";
|
||||
import type { ChatQueueItem } from "../../../lib/chat/chat-types.ts";
|
||||
@@ -92,6 +92,8 @@ export type ChatComposerProps = ChatAttachmentControlsProps & {
|
||||
stream: string | null;
|
||||
queue: ChatQueueItem[];
|
||||
draft: string;
|
||||
modelCatalog: readonly ModelCatalogEntry[];
|
||||
modelSwitching: boolean;
|
||||
sessions: SessionsListResult | null;
|
||||
toolOverrides?: SessionToolOverrides;
|
||||
capabilityMenu?: CapabilityMenuProps;
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
getActiveSlashMenuOptionId,
|
||||
getActiveSlashMenuOptionLabel,
|
||||
isSlashMenuVisible,
|
||||
resetSlashMenuState,
|
||||
updateSlashMenu,
|
||||
} from "./chat-composer-slash-menu.ts";
|
||||
import {
|
||||
@@ -513,6 +514,10 @@ export function renderChatComposer(props: ChatComposerProps) {
|
||||
?.getVideoTracks?.()[0]
|
||||
?.getSettings?.().facingMode;
|
||||
const mirrorCameraPreview = cameraFacingMode !== "environment";
|
||||
if (props.modelSwitching && state.slashMenuCommand?.key === "think") {
|
||||
state.slashMenuOpen = false;
|
||||
resetSlashMenuState(state);
|
||||
}
|
||||
const slashMenuVisible = props.connected && canCompose && isSlashMenuVisible(state);
|
||||
const skillMenuVisible = props.connected && canCompose && isSkillMenuVisible(state);
|
||||
if (!skillMenuVisible && state.skillMenuOpen && !state.skillCommandRefreshPending) {
|
||||
|
||||
Reference in New Issue
Block a user