mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(agents): show a pending question in full in the permission banner (#1111)
The permission banner kept its text on one line, so an agent's question (Claude Code's AskUserQuestion arrives as a permission request) was cut off and its answers crowded into the rest of the line. The question now wraps and scrolls past a third of the editor's height, and the answers wrap after it; permission lines in the conversation show up to three lines with the full text as tooltip.
This commit is contained in:
@@ -77,27 +77,41 @@
|
||||
color: var(--fg-subtle);
|
||||
}
|
||||
|
||||
/* Pending permission: an editor notification banner. */
|
||||
/* Pending permission: an editor notification banner. A long question (AskUserQuestion) wraps,
|
||||
scrolling past a third of the height; the answers follow it, wrapping too. */
|
||||
.banner {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 4px 8px;
|
||||
min-height: 36px;
|
||||
padding: 0 12px;
|
||||
padding: 9px 12px;
|
||||
background: var(--bg-feedback-warning);
|
||||
border-bottom: 1px solid var(--border-feedback-warning);
|
||||
color: var(--fg-strong);
|
||||
}
|
||||
.banner > svg {
|
||||
flex: none;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.bannerText {
|
||||
flex: 1 1 240px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-height: 33vh;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.bannerSpacer {
|
||||
flex: 1;
|
||||
}
|
||||
.bannerActions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.bannerLink {
|
||||
flex: none;
|
||||
padding: 0;
|
||||
@@ -430,16 +444,24 @@
|
||||
|
||||
.permissionLine {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
/* Up to three lines; the full text is its tooltip. */
|
||||
.permissionTitle {
|
||||
display: -webkit-box;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.permissionLine > svg,
|
||||
.permissionLine > .muted {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
|
||||
@@ -1151,7 +1151,9 @@ function PermissionLine({ task, entry }: { task: AgentTask; entry: Extract<Agent
|
||||
return (
|
||||
<div className={styles.permissionLine}>
|
||||
<Icon name={!entry.chosen ? "warning" : entry.chosen === "Cancelled" ? "minus" : "check"} size={13} />
|
||||
<span className={styles.permissionTitle}>{shortTitle(task, entry.title)}</span>
|
||||
<span className={styles.permissionTitle} title={shortTitle(task, entry.title)}>
|
||||
{shortTitle(task, entry.title)}
|
||||
</span>
|
||||
<span className={styles.muted}>{entry.chosen ?? <T>Waiting for your answer</T>}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -1171,15 +1173,14 @@ function PermissionBanner({ task, entries }: { task: AgentTask; entries: AgentEn
|
||||
return (
|
||||
<div className={styles.banner} role="alert" aria-label={entry.title}>
|
||||
<Icon name="warning" size={16} />
|
||||
<span className={styles.bannerText} title={entry.title}>
|
||||
{shortTitle(task, entry.title)}
|
||||
<span className={styles.bannerText}>{shortTitle(task, entry.title)}</span>
|
||||
<span className={styles.bannerActions}>
|
||||
{entry.options.map((o) => (
|
||||
<button key={o.id} type="button" className={styles.bannerLink} disabled={busy} onClick={() => answer(o.id)}>
|
||||
{o.name}
|
||||
</button>
|
||||
))}
|
||||
</span>
|
||||
<span className={styles.bannerSpacer} />
|
||||
{entry.options.map((o) => (
|
||||
<button key={o.id} type="button" className={styles.bannerLink} disabled={busy} onClick={() => answer(o.id)}>
|
||||
{o.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ import { disconnect, useAgents } from "./store";
|
||||
import { taskActions } from "./taskActions";
|
||||
import styles from "./Session.module.css";
|
||||
|
||||
const backend = vi.hoisted(() => ({ entries: new Map<string, AgentEntry[]>(), changes: [] as AgentChange[] }));
|
||||
const backend = vi.hoisted(() => ({
|
||||
entries: new Map<string, AgentEntry[]>(),
|
||||
changes: [] as AgentChange[],
|
||||
answers: [] as Array<[string, number, string | null]>,
|
||||
}));
|
||||
vi.mock("@/platform/ipc", async (original) => ({
|
||||
...(await original<typeof import("@/platform/ipc")>()),
|
||||
commands: {
|
||||
@@ -20,6 +24,10 @@ vi.mock("@/platform/ipc", async (original) => ({
|
||||
}),
|
||||
agentChanges: async () => ({ status: "ok", data: backend.changes }),
|
||||
vcsBranchInfo: async () => ({ status: "ok", data: { head: "main" } }),
|
||||
agentRespond: async (task: string, permission: number, option: string | null) => {
|
||||
backend.answers.push([task, permission, option]);
|
||||
return { status: "ok", data: null };
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("@tauri-apps/api/webview", () => ({
|
||||
@@ -93,6 +101,40 @@ beforeEach(() => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("a pending question", () => {
|
||||
it("shows the whole question in the banner, with every answer, and sends the chosen one", async () => {
|
||||
const question =
|
||||
'Scenario 4 ("follows the TR") still leaves the "About your request" section empty after two rewrites.\nHow should we continue?';
|
||||
const entries: AgentEntry[] = [
|
||||
{
|
||||
type: "permission",
|
||||
id: 7,
|
||||
title: question,
|
||||
options: [
|
||||
{ id: "a", name: "Ship it as is and record it (Recommended)", kind: "allow_once" },
|
||||
{ id: "b", name: "Detect the request in code", kind: "allow_once" },
|
||||
{ id: "c", name: "Skip", kind: "reject_once" },
|
||||
],
|
||||
chosen: null,
|
||||
},
|
||||
];
|
||||
backend.entries.set("A", entries);
|
||||
backend.answers = [];
|
||||
useAgents.setState({ entries: new Map(backend.entries) });
|
||||
render(<SessionEditor input={input("A")} />);
|
||||
const banner = await screen.findByRole("alert", { name: question });
|
||||
expect(banner.querySelector(`.${styles.bannerText}`)?.textContent).toBe(question);
|
||||
const answers = within(banner.querySelector<HTMLElement>(`.${styles.bannerActions}`)!).getAllByRole("button");
|
||||
expect(answers.map((b) => b.textContent)).toEqual([
|
||||
"Ship it as is and record it (Recommended)",
|
||||
"Detect the request in code",
|
||||
"Skip",
|
||||
]);
|
||||
fireEvent.click(answers[1]!);
|
||||
await waitFor(() => expect(backend.answers).toEqual([["A", 7, "b"]]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("session state across tasks and hosts", () => {
|
||||
it("returns to the task conversation when the docked task changes after opening a subagent", async () => {
|
||||
const dispose = registerActions(taskActions);
|
||||
|
||||
Reference in New Issue
Block a user