Compare commits

..
Author SHA1 Message Date
l0ng-ai 9bcbb09027 fix(agents): keep list selection unbroken in agent replies
Draw list markers as non-selectable pseudo-elements instead of native
markers. WebKit paints a selection box around each native marker, which
left a broken stripe down the list gutter when a reply was selected.
2026-11-11 12:27:34 +08:00
3 changed files with 28 additions and 37 deletions

No files matched your search

+28 -1
View File
@@ -471,6 +471,31 @@
.markdown :global(ol) {
margin: 0 0 8px;
padding-left: 20px;
list-style: none;
}
/* Markers are drawn as pseudo-elements: WebKit paints a selection box around a native marker,
leaving a broken stripe down the gutter when a list is selected. */
.markdown :global(li) {
position: relative;
}
.markdown :global(li)::before {
position: absolute;
right: calc(100% + 6px);
-webkit-user-select: none;
user-select: none;
}
.markdown :global(ul > li)::before {
content: "•";
}
.markdown :global(ul ul > li)::before {
content: "◦";
}
.markdown :global(ul ul ul > li)::before {
content: "▪";
}
.markdown :global(ol > li)::before {
content: counter(list-item) ".";
font-variant-numeric: tabular-nums;
}
.markdown :global(a) {
color: var(--link);
@@ -521,7 +546,9 @@
}
.markdown :global(li:has(> input[type="checkbox"])) {
margin-left: -20px;
list-style: none;
}
.markdown :global(li:has(> input[type="checkbox"]))::before {
content: none;
}
.markdown > :global(:first-child) {
margin-top: 0;
-23
View File
@@ -129,15 +129,6 @@ export function catalogState(
const vendorOf = (authors: readonly string[]) => authors.join(", ");
/** The marketplace's newer version of an installed plugin, if there is one this IDE can load. */
function useMarketUpdate(plugin: PluginInfo): MarketPlugin | undefined {
return usePlugins((s) =>
plugin.source === "installed"
? s.market?.find((m) => m.id === plugin.id && m.compatible && compareVersions(m.version, plugin.version) > 0)
: undefined,
);
}
function PluginIcon({ id, icon, large }: { id: string; icon: PluginIconData | null | undefined; large?: boolean }) {
return (
<span className={clsx(styles.icon, large && styles.iconLarge)} aria-hidden>
@@ -157,7 +148,6 @@ function ListHeader({ title, count }: { title: string; count: number }) {
function InstalledRow({ plugin, selected, onSelect }: { plugin: PluginInfo; selected: boolean; onSelect: () => void }) {
const busy = usePlugins((s) => !!s.busy[plugin.id]);
const update = useMarketUpdate(plugin);
return (
<li className={clsx(styles.row, selected && styles.rowSelected, !plugin.enabled && styles.rowDisabled)}>
<button
@@ -176,7 +166,6 @@ function InstalledRow({ plugin, selected, onSelect }: { plugin: PluginInfo; sele
{plugin.source !== "installed" && (
<span className={styles.devTag}>{plugin.source === "linked" ? tx("linked") : tx("dev")}</span>
)}
{update && <Badge tone="accent">{tx("Update")}</Badge>}
{plugin.status === "failed" && (
<Badge dot tone="danger">
<T>Failed</T>
@@ -555,8 +544,6 @@ function Homepage({ value }: { value: string }) {
function InstalledDetails({ plugin }: { plugin: PluginInfo }) {
const busy = usePlugins((s) => !!s.busy[plugin.id]);
const installing = usePlugins((s) => s.installing);
const update = useMarketUpdate(plugin);
const status = STATUS[plugin.status];
const [confirmUninstall, setConfirmUninstall] = useState(false);
@@ -659,16 +646,6 @@ function InstalledDetails({ plugin }: { plugin: PluginInfo }) {
</div>
<div className={styles.headerActions}>
{busy && <Spinner size={14} label="Working" />}
{update && (
<Button
size="small"
variant="primary"
disabled={busy || installing}
onClick={() => void installFromMarket(update.id)}
>
{tx`Update to ${update.version}`}
</Button>
)}
<Button
size="small"
variant={plugin.enabled ? "secondary" : "primary"}
@@ -1354,19 +1354,6 @@ describe("marketplace", () => {
await waitFor(() => expect(consents.map(([id]) => id)).toEqual(["a", "b"]));
});
it("marks installed plugins with updates and updates one from its details", async () => {
setConsentPrompt(async () => ({ optional: [] }));
const consents = updatesBackend({ a: preview("a", "1.1.0"), b: preview("b", "2.0.0") });
const user = userEvent.setup();
render(<PluginsPage />);
await waitFor(() => expect(usePlugins.getState().market).toHaveLength(3));
const list = screen.getByRole("list", { name: "Installed plugins" });
expect(within(list).getAllByText("Update")).toHaveLength(2);
await user.click(within(list).getAllByRole("button", { name: /^b/i })[0]!);
await user.click(await screen.findByRole("button", { name: "Update to 2.0.0" }));
await waitFor(() => expect(consents.map(([id]) => id)).toEqual(["b"]));
});
it("announces updates with actions to install them or show the plugins", async () => {
updatesBackend({ a: preview("a", "1.1.0"), b: preview("b", "2.0.0") });
handlePluginEvent({