feat(agents): combine model and effort, and mode and Allow All, in the composer (#958)

- One composer button shows the model and its effort; its popup has the searchable model list and a stepped effort slider over the levels the agent advertises.
- With an on/off permissions option (Allow All), the mode menu carries it as a switch, and new tasks start with it on like the other bypass settings.
- Recognise ACP session-mode URIs when picking mode icons.
- The Stop action in a running session is an icon button.
Цей коміт міститься в:
Deng Liu authored and GitHub committed 2026-11-05 14:20:26 +08:00
1 джерело 566190e94b
коміт 2b86dd2d9f
8 files changed
+564 -30

No files matched your search

+140
Переглянути файл
@@ -94,6 +94,146 @@
color: var(--fg-muted);
overflow-wrap: anywhere;
}
/* A ghost button showing a value and its detail, like a ghost Select. */
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
height: var(--h-control);
min-width: 0;
padding: 0 4px 0 6px;
border-radius: var(--radius-control);
color: var(--fg-secondary);
white-space: nowrap;
transition: background var(--dur-fast) var(--ease);
}
.chip:hover,
.chip[aria-expanded="true"] {
background: var(--bg-toolbar-hover);
color: var(--fg-strong);
}
.chipValue {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.chipDetail {
display: inline-grid;
color: var(--fg-subtle);
}
.chipDetail > span {
grid-area: 1 / 1;
}
.chipSizer {
visibility: hidden;
}
.chipChevron {
flex: none;
color: var(--fg-muted);
}
.modelEffortPopup {
width: min(300px, calc(100vw - 24px));
padding: 4px 12px 12px;
}
.effortHeader {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-top: 12px;
}
.effortValue {
color: var(--fg-strong);
}
.effortSlider {
--thumb: 14px;
margin-top: 8px;
}
.effortSlider input {
display: block;
width: 100%;
height: var(--thumb);
margin: 0;
appearance: none;
background: transparent;
}
.effortSlider input::-webkit-slider-runnable-track {
height: 4px;
border-radius: 2px;
background: linear-gradient(to right, var(--accent) var(--fill), var(--control-off) var(--fill));
}
.effortSlider input::-webkit-slider-thumb {
width: var(--thumb);
height: var(--thumb);
margin-top: calc((4px - var(--thumb)) / 2);
border-radius: 50%;
appearance: none;
background: var(--fg-on-accent);
box-shadow:
0 0 0 1px var(--border-button),
var(--shadow-tooltip);
}
.effortSlider input:focus-visible {
outline: none;
}
.effortSlider input:focus-visible::-webkit-slider-thumb {
box-shadow: 0 0 0 2px var(--focus-ring);
}
/* One dot per level, centred under the thumb's positions. */
.effortTicks {
display: flex;
justify-content: space-between;
padding: 0 calc(var(--thumb) / 2 - 2px);
margin-top: 4px;
}
.effortTicks > span {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--control-off);
}
.effortTicks > .effortTickActive {
background: var(--accent);
}
.modePopup {
min-width: 200px;
padding: 6px 0;
}
.modeList {
display: flex;
flex-direction: column;
}
.modeOption {
display: flex;
align-items: center;
gap: 6px;
height: 24px;
margin: 0 6px;
padding: 0 8px;
border-radius: var(--radius-row);
color: var(--fg-strong);
text-align: left;
white-space: nowrap;
}
.modeOption:hover,
.modeOption:focus-visible {
outline: none;
background: var(--bg-list-selected);
}
.modeCheck {
width: 12px;
flex: none;
color: var(--fg-muted);
}
.modeFooter {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 6px;
padding: 8px 14px 2px;
border-top: 1px solid var(--border);
}
@container agent-composer (max-width: 600px) {
.bar {
flex-direction: column;
+134
Переглянути файл
@@ -78,6 +78,63 @@ it("shows Cursor approvals separately from its execution mode and sends the choi
expect(onChange).toHaveBeenCalledWith("openidea.cursor.approval", "bypass");
});
it("shows Allow All as a switch and sends on/off values", () => {
const onChange = vi.fn();
render(
<ConfigPickers
config={[
{
id: "allow_all",
name: "Allow All",
category: "permissions",
description: null,
current: "off",
via: "option",
values: [value("on", "On"), value("off", "Off")],
},
]}
onChange={onChange}
/>,
);
fireEvent.click(screen.getByRole("switch", { name: "Allow All" }));
expect(onChange).toHaveBeenCalledExactlyOnceWith("allow_all", "on");
});
it("shows Allow All inside the mode popup when both options exist", () => {
const onChange = vi.fn();
render(
<ConfigPickers
config={[
{
id: "mode",
name: "Mode",
category: "mode",
description: null,
current: "agent",
via: "option",
values: [value("agent", "Agent"), value("plan", "Plan"), value("autopilot", "Autopilot")],
},
{
id: "allow_all",
name: "Allow All",
category: "permissions",
description: null,
current: "off",
via: "option",
values: [value("on", "On"), value("off", "Off")],
},
]}
onChange={onChange}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Mode" }));
fireEvent.click(screen.getByRole("option", { name: "Plan" }));
expect(onChange).toHaveBeenNthCalledWith(1, "mode", "plan");
fireEvent.click(screen.getByRole("button", { name: "Mode" }));
fireEvent.click(screen.getByRole("switch", { name: "Allow All" }));
expect(onChange).toHaveBeenNthCalledWith(2, "allow_all", "on");
});
it("sends the complete nested model id through ConfigPickers", () => {
const onChange = vi.fn();
render(<ConfigPickers config={[config([value("9router/cmc/Beam")])]} onChange={onChange} />);
@@ -110,3 +167,80 @@ it("edits secondary settings from the compact popup and restores focus when dism
expect(screen.queryByRole("dialog", { name: "Agent settings" })).not.toBeInTheDocument();
expect(trigger).toHaveFocus();
});
describe("model and effort button", () => {
const copilotConfig = (effortLevels: string[] | null = ["low", "medium", "high", "xhigh"]): AgentConfigOption[] => [
{
id: "model",
name: "Model",
category: "model",
description: null,
current: "claude-sonnet-5.5",
via: "option",
values: [value("claude-sonnet-5.5", "Claude Sonnet 5.5", "1x"), value("gpt-6-sol", "GPT-6 Sol", "1x")],
},
...(effortLevels
? [
{
id: "reasoning_effort",
name: "Reasoning Effort",
category: "thought_level",
description: null,
current: "medium",
via: "option" as const,
values: effortLevels.map((lvl) => value(lvl, lvl === "xhigh" ? "XHigh" : lvl)),
},
]
: []),
{
id: "mode",
name: "Mode",
category: "mode",
description: null,
current: "https://agentclientprotocol.com/protocol/session-modes#agent",
via: "option",
values: [
value("https://agentclientprotocol.com/protocol/session-modes#agent", "Agent"),
value("https://agentclientprotocol.com/protocol/session-modes#plan", "Plan"),
],
},
{
id: "allow_all",
name: "Allow All",
category: "permissions",
description: null,
current: "on",
via: "option",
values: [value("on", "On"), value("off", "Off")],
},
];
it("shows the model with the agent's effort name and the mode with Allow All", () => {
render(<ConfigPickers config={copilotConfig()} onChange={vi.fn()} />);
const model = screen.getByRole("button", { name: "Model" });
expect(model).toHaveTextContent("Claude Sonnet 5.5medium");
expect(screen.getByRole("button", { name: "Mode" })).toHaveTextContent("AgentAllow All");
expect(screen.queryByRole("button", { name: "Reasoning Effort" })).not.toBeInTheDocument();
});
it("steps through the advertised effort levels and picks a model from the popup", () => {
const onChange = vi.fn();
render(<ConfigPickers config={copilotConfig()} values={{ reasoning_effort: "high" }} onChange={onChange} />);
fireEvent.click(screen.getByRole("button", { name: "Model" }));
const dialog = screen.getByRole("dialog", { name: "Model & effort settings" });
const slider = within(dialog).getByRole("slider", { name: "Reasoning Effort" });
expect(slider).toHaveAttribute("max", "3");
expect(slider).toHaveValue("2");
expect(slider).toHaveAttribute("aria-valuetext", "high");
fireEvent.change(slider, { target: { value: "3" } });
expect(onChange).toHaveBeenLastCalledWith("reasoning_effort", "xhigh");
fireEvent.click(within(dialog).getByRole("button", { name: "Model" }));
fireEvent.click(screen.getByRole("option", { name: /GPT-6 Sol/ }));
expect(onChange).toHaveBeenLastCalledWith("model", "gpt-6-sol");
});
it("keeps a plain model picker when the agent has no effort option", () => {
render(<ConfigPickers config={copilotConfig(null)} onChange={vi.fn()} />);
expect(screen.getByRole("button", { name: "Model" })).toHaveAttribute("aria-haspopup", "listbox");
});
});
+249 -15
Переглянути файл
@@ -1,12 +1,14 @@
/**
* Model, mode, approvals and thinking-effort dropdowns for a composer's bottom row.
* Modes and approvals use icons; levels are plain text.
* Model, mode, approvals and thinking-effort controls for a composer's bottom row.
* With both a model and an effort option, one button shows both and opens the model
* list and an effort slider; with a mode and an on/off permissions option (Allow All),
* the mode menu carries the switch. Names and descriptions are the agent's own.
*/
import { useRef, useState } from "react";
import { useRef, useState, type CSSProperties } from "react";
import clsx from "clsx";
import type { AgentConfigOption } from "@/bindings";
import { useTx } from "@/platform/i18n";
import { IconButton, Popup, Select, T } from "@/ui";
import { Icon, IconButton, Popup, Select, T, Toggle } from "@/ui";
import type { SelectOption } from "@/ui/Select";
import { mainOptions, modeIcon } from "./composerModel";
import styles from "./Composer.module.css";
@@ -34,6 +36,17 @@ export function configPickerOptions(option: AgentConfigOption): SelectOption[] {
];
}
/** The on and off values of a two-state permissions option, or null when it has other values. */
function switchValues(option: AgentConfigOption): { on: string; off: string } | null {
if (option.category !== "permissions") return null;
const on = option.values.find((v) => /^(on|true)$/i.test(v.value))?.value;
const off = option.values.find((v) => /^(off|false)$/i.test(v.value))?.value;
return on && off ? { on, off } : null;
}
const selected = (option: AgentConfigOption, value: string | null) =>
option.values.find((v) => v.value === value) ?? null;
export function ConfigPickers({
config,
values,
@@ -48,26 +61,46 @@ export function ConfigPickers({
const [open, setOpen] = useState(false);
const anchor = useRef<HTMLButtonElement>(null);
const options = mainOptions(config);
const secondary = options.filter((o) => o.category !== "model");
const valueOf = (o: AgentConfigOption) => values?.[o.id] ?? o.current ?? null;
const model = options.find((o) => o.category === "model");
const effort = model && options.find((o) => o.category === "thought_level" && o.values.length > 0);
const mode = options.find((o) => o.category === "mode");
const allowAll = mode && options.find((o) => switchValues(o));
const inline = options.filter((o) => o !== effort && o !== allowAll);
// The compact menu holds everything but the model button (which carries the effort).
const secondary = options.filter((o) => o.category !== "model" && o !== effort);
const approval = secondary.find((o) => o.category === "approval" || o.category === "mode");
const summary = secondary
.map((o) => {
const value = values?.[o.id] ?? o.current;
return `${tx(o.name)}: ${o.values.find((v) => v.value === value)?.name ?? tx("Default")}`;
})
.join(" · ");
const summary = secondary.map((o) => `${tx(o.name)}: ${selected(o, valueOf(o))?.name ?? tx("Default")}`).join(" · ");
return (
<>
{options.map((o) => (
{inline.map((o) => (
<span key={o.id} className={clsx(styles.configPicker, o.category !== "model" && styles.inlineSetting)}>
<ConfigPicker option={o} value={values?.[o.id] ?? o.current ?? null} onChange={onChange} />
{o === model && effort ? (
<ModelEffortPicker
model={o}
modelValue={valueOf(o)}
effort={effort}
effortValue={valueOf(effort)}
onChange={onChange}
/>
) : o === mode && allowAll ? (
<ModePicker
mode={o}
modeValue={valueOf(o)}
allowAll={allowAll}
allowAllValue={valueOf(allowAll)}
onChange={onChange}
/>
) : (
<ConfigPicker option={o} value={valueOf(o)} onChange={onChange} />
)}
</span>
))}
{secondary.length > 0 && (
<div className={styles.settingsMenu}>
<IconButton
ref={anchor}
icon={approval ? modeIcon(values?.[approval.id] ?? approval.current ?? "") : "settings"}
icon={approval ? modeIcon(valueOf(approval) ?? "") : "settings"}
title={`${tx("Agent settings")}: ${summary}`}
aria-label={tx("Agent settings")}
aria-haspopup="dialog"
@@ -88,7 +121,7 @@ export function ConfigPickers({
{secondary.map((o) => (
<div key={o.id} className={styles.settingsRow}>
<span className={styles.settingsLabel}>{tx(o.name)}</span>
<ConfigPicker option={o} value={values?.[o.id] ?? o.current ?? null} onChange={onChange} framed />
<ConfigPicker option={o} value={valueOf(o)} onChange={onChange} framed />
</div>
))}
</Popup>
@@ -110,6 +143,17 @@ function ConfigPicker({
framed?: boolean;
}) {
const tx = useTx();
const toggle = switchValues(option);
if (toggle) {
return (
<Toggle
checked={value === toggle.on}
aria-label={tx(option.name)}
label={framed ? undefined : tx(option.name)}
onChange={(checked) => onChange(option.id, checked ? toggle.on : toggle.off)}
/>
);
}
return (
<Select
variant={framed ? "framed" : "ghost"}
@@ -142,3 +186,193 @@ function ConfigPicker({
/>
);
}
/** One button for the model and its effort; the popup has the model list and an effort slider. */
function ModelEffortPicker({
model,
modelValue,
effort,
effortValue,
onChange,
}: {
model: AgentConfigOption;
modelValue: string | null;
effort: AgentConfigOption;
effortValue: string | null;
onChange: (option: string, value: string) => void;
}) {
const tx = useTx();
const [open, setOpen] = useState(false);
const anchor = useRef<HTMLButtonElement>(null);
const modelName = selected(model, modelValue)?.name ?? tx("Default");
const effortName = selected(effort, effortValue)?.name ?? tx("Default");
const summary = `${tx(model.name)}: ${modelName} · ${tx(effort.name)}: ${effortName}`;
return (
<>
<button
ref={anchor}
type="button"
className={styles.chip}
aria-label={tx(model.name)}
aria-haspopup="dialog"
aria-expanded={open}
title={summary}
onClick={() => setOpen((v) => !v)}
>
<span className={styles.chipValue}>{modelName}</span>
{/* Every level's name in one cell, so the button keeps the widest one's width. */}
<span className={styles.chipDetail}>
{[...new Set([effortName, ...effort.values.map((v) => v.name)])].map((name) => (
<span
key={name}
className={name === effortName ? undefined : styles.chipSizer}
aria-hidden={name !== effortName}
>
{name}
</span>
))}
</span>
<Icon name="chevron-down" size={14} className={styles.chipChevron} />
</button>
<Popup
open={open}
anchor={anchor}
onClose={() => setOpen(false)}
aria-label={tx("Model & effort settings")}
className={styles.modelEffortPopup}
>
<div className={styles.settingsRow}>
<span className={styles.settingsLabel}>{tx(model.name)}</span>
<ConfigPicker option={model} value={modelValue} onChange={onChange} framed />
</div>
<div className={styles.effortHeader}>
<span className={styles.settingsLabel}>{tx(effort.name)}</span>
<span className={styles.effortValue}>{effortName}</span>
</div>
<EffortSlider option={effort} value={effortValue} onChange={(v) => onChange(effort.id, v)} />
</Popup>
</>
);
}
/** A stepped slider over the agent's effort levels, in the order the agent lists them. */
function EffortSlider({
option,
value,
onChange,
}: {
option: AgentConfigOption;
value: string | null;
onChange: (value: string) => void;
}) {
const tx = useTx();
const steps = option.values;
const index = Math.max(
0,
steps.findIndex((v) => v.value === value),
);
const last = steps.length - 1;
const fill = last > 0 ? (index / last) * 100 : 100;
return (
<div className={styles.effortSlider}>
<input
type="range"
min={0}
max={last}
step={1}
value={index}
aria-label={tx(option.name)}
aria-valuetext={steps[index]?.name}
title={steps[index]?.description ?? undefined}
style={{ "--fill": `${fill}%` } as CSSProperties}
onChange={(e) => {
const next = steps[Number(e.target.value)];
if (next && next.value !== value) onChange(next.value);
}}
/>
<div className={styles.effortTicks} aria-hidden>
{steps.map((v) => (
<span key={v.value} className={clsx(v.value === steps[index]?.value && styles.effortTickActive)} />
))}
</div>
</div>
);
}
/** The mode menu, with the agent's on/off permissions option (Allow All) as a switch under it. */
function ModePicker({
mode,
modeValue,
allowAll,
allowAllValue,
onChange,
}: {
mode: AgentConfigOption;
modeValue: string | null;
allowAll: AgentConfigOption;
allowAllValue: string | null;
onChange: (option: string, value: string) => void;
}) {
const tx = useTx();
const [open, setOpen] = useState(false);
const anchor = useRef<HTMLButtonElement>(null);
const toggle = switchValues(allowAll)!;
const current = selected(mode, modeValue);
const on = allowAllValue === toggle.on;
const summary = `${tx(mode.name)}: ${current?.name ?? tx("Default")} · ${tx(allowAll.name)}: ${on ? tx("On") : tx("Off")}`;
return (
<>
<button
ref={anchor}
type="button"
className={styles.chip}
aria-label={tx(mode.name)}
aria-haspopup="dialog"
aria-expanded={open}
title={summary}
onClick={() => setOpen((v) => !v)}
>
<Icon name={on ? "unlock" : modeIcon(current?.value ?? "")} size={14} />
<span className={styles.chipValue}>{current?.name ?? tx("Default")}</span>
{on && <span className={styles.chipDetail}>{tx(allowAll.name)}</span>}
<Icon name="chevron-down" size={14} className={styles.chipChevron} />
</button>
<Popup
open={open}
anchor={anchor}
onClose={() => setOpen(false)}
aria-label={tx(mode.name)}
className={styles.modePopup}
>
<div role="listbox" aria-label={tx(mode.name)} className={styles.modeList}>
{mode.values.map((v) => (
<button
key={v.value}
type="button"
role="option"
aria-selected={v.value === current?.value}
title={v.description ?? undefined}
className={styles.modeOption}
onClick={() => {
onChange(mode.id, v.value);
setOpen(false);
}}
>
<span className={styles.modeCheck}>{v.value === current?.value && <Icon name="check" size={12} />}</span>
<Icon name={modeIcon(v.value)} size={14} />
<span className={styles.chipValue}>{v.name}</span>
</button>
))}
</div>
<div className={styles.modeFooter}>
<span title={allowAll.description ?? undefined}>{tx(allowAll.name)}</span>
<Toggle
checked={on}
aria-label={tx(allowAll.name)}
onChange={(checked) => onChange(allowAll.id, checked ? toggle.on : toggle.off)}
/>
</div>
</Popup>
</>
);
}
+1 -5
Переглянути файл
@@ -1295,11 +1295,7 @@ function SessionComposer({
toolbar={<ConfigPickers config={task.config} onChange={setOption} />}
actions={
<>
{busy && (
<Button size="small" icon="stop" onClick={() => void cancelTask(task.id)}>
Stop
</Button>
)}
{busy && <IconButton icon="stop" title="Stop" size="small" onClick={() => void cancelTask(task.id)} />}
<SubmitButton
label={busy ? "Queue" : "Send"}
pending={sending}
+24 -3
Переглянути файл
@@ -3,6 +3,7 @@ import type { AgentConfigOption, AgentEntry, AgentStatus, AgentTask } from "@/bi
import {
applyCompletion,
atEdgeLine,
bypassDefaults,
canSend,
completionAt,
isPathQuery,
@@ -157,11 +158,31 @@ describe("agent composer", () => {
const config = [option("fast", ["on"], null), option("mode", ["ask", "code"], "code"), option("model", ["m"], "m")];
expect(mainOptions(config).map((o) => o.id)).toEqual(["model", "mode"]);
expect(nextValue(config[1]!)).toBe("ask");
const permissions = option("allow_all", ["off", "on"], "off");
permissions.category = "permissions";
expect(mainOptions([...config, permissions]).map((o) => o.id)).toEqual(["model", "mode", permissions.id]);
const approvals = { ...option("openidea.cursor.approval", ["ask", "bypass"], "ask"), category: "approval" };
expect(mainOptions([...config, approvals]).map((o) => o.id)).toEqual(["model", "mode", approvals.id]);
expect(mainOptions([...config, permissions, approvals]).map((o) => o.id)).toEqual([
"model",
"mode",
permissions.id,
approvals.id,
]);
expect(
["default", "acceptEdits", "plan", "auto", "bypassPermissions", "auto_edit", "read-only", "x"].map(modeIcon),
).toEqual(["lock", "edit", "todo", "run", "unlock", "edit", "eye", "lock"]);
[
"default",
"acceptEdits",
"plan",
"auto",
"bypassPermissions",
"auto_edit",
"read-only",
"x",
"https://agentclientprotocol.com/protocol/session-modes#agent",
"https://agentclientprotocol.com/protocol/session-modes#autopilot",
].map(modeIcon),
).toEqual(["lock", "edit", "todo", "run", "unlock", "edit", "eye", "lock", "run", "run"]);
expect(bypassDefaults([...config, permissions])).toEqual({ allow_all: "on" });
expect(usageLabel({ used: 24_000, size: 200_000 })).toEqual({
short: "12%",
detail: "24k / 200k tokens",
+12 -4
Переглянути файл
@@ -152,9 +152,9 @@ export function atEdgeLine(text: string, caret: number, dir: -1 | 1): boolean {
return dir < 0 ? !text.slice(0, caret).includes("\n") : !text.slice(caret).includes("\n");
}
/** The settings shown as dropdowns: model, mode, approvals, thinking effort, in that order. */
/** The settings shown in the composer: model, mode, permissions, approvals, effort. */
export function mainOptions(config: readonly AgentConfigOption[]): AgentConfigOption[] {
const order = ["model", "mode", "approval", "thought_level"];
const order = ["model", "mode", "permissions", "approval", "thought_level"];
return order.flatMap((c) => config.filter((o) => o.category === c).slice(0, 1));
}
@@ -165,12 +165,17 @@ export const modeOption = (config: readonly AgentConfigOption[]) => config.find(
* Claude `acceptEdits`, Gemini `auto_edit`, Codex `full-access`…). Unknown modes ask.
*/
export function modeIcon(mode: string): string {
const m = mode.toLowerCase().replace(/[^a-z]/g, "");
const m =
mode
.toLowerCase()
.split(/[#/]/)
.at(-1)
?.replace(/[^a-z]/g, "") ?? "";
if (m.includes("plan") || m === "architect") return "todo";
if (m.includes("bypass") || m === "yolo" || m.includes("fullaccess") || m === "dangerous") return "unlock";
if (m.includes("readonly") || m === "ask" || m === "chat") return "eye";
if (m.includes("edit") || m === "code") return "edit";
if (m === "auto" || m === "agent") return "run";
if (m === "auto" || m === "agent" || m.includes("autopilot")) return "run";
return "lock";
}
@@ -180,6 +185,9 @@ export function bypassDefaults(config: readonly AgentConfigOption[]): Record<str
const mode = modeOption(config);
const unlock = mode?.values.find((v) => modeIcon(v.value) === "unlock");
if (mode && unlock) out[mode.id] = unlock.value;
const permissions = config.find((o) => o.category === "permissions");
const allowAll = permissions?.values.find((v) => /^(on|true)$/i.test(v.value));
if (permissions && allowAll) out[permissions.id] = allowAll.value;
const approval = config.find((o) => o.category === "approval");
if (approval?.values.some((v) => v.value === "bypass")) out[approval.id] = "bypass";
return out;
+2 -2
Переглянути файл
@@ -256,14 +256,14 @@ describe("session composer submission", () => {
expect(box).toHaveValue("");
});
it("keeps Queue named and leaves Stop unchanged while the agent works", async () => {
it("keeps Queue named and shows icon-only Stop actions while the agent works", async () => {
useAgents.setState({ tasks: new Map([[task.id, { ...task, status: "running" }]]) });
render(<SessionEditor input={{ kind: "agent-session", key: task.id, data: { task: task.id } }} />);
const queue = screen.getByRole("button", { name: "Queue" });
expect(queue.textContent).toBe("");
expect(queue).toHaveAttribute("data-tooltip", "Queue");
expect(queue).toBeDisabled();
expect(screen.getAllByRole("button", { name: "Stop" }).some((button) => button.textContent === "Stop")).toBe(true);
expect(screen.getAllByRole("button", { name: "Stop" }).every((button) => button.textContent === "")).toBe(true);
await screen.findByText("Original message");
});
});
+2 -1
Переглянути файл
@@ -6219,5 +6219,6 @@
"First Page": "第一页",
"Count rows": "统计行数",
"Last Page": "最后一页",
"{first}-{last} of {total}": "{first}-{last},共 {total}"
"{first}-{last} of {total}": "{first}-{last},共 {total}",
"Model & effort settings": "模型与推理强度"
}