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
9 changed files with 108 additions and 7 deletions

No files matched your search

+59 -1
View File
@@ -54,6 +54,10 @@
align-items: center;
gap: 2px;
}
.statusActions > .changesToggle {
height: var(--h-control-small);
padding: 0 4px;
}
.statusText {
flex: 1 1 0;
min-width: 0;
@@ -467,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);
@@ -517,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;
@@ -1226,6 +1257,33 @@
text-overflow: ellipsis;
}
.changesToggle {
display: inline-flex;
align-items: center;
gap: 4px;
height: var(--h-control);
padding: 0 8px;
border: 0;
border-radius: var(--radius-small);
background: transparent;
color: var(--fg-subtle);
font: inherit;
font-size: var(--fs-small);
cursor: pointer;
}
.changesToggle:hover:not(:disabled) {
background: var(--bg-hover);
color: var(--fg-strong);
}
.changesToggle[aria-pressed="true"] {
background: var(--bg-selected);
color: var(--fg-strong);
}
.changesToggle:disabled {
opacity: 0.4;
cursor: default;
}
/* An open AskUserQuestion: every question with its options, answered at once above the composer. */
.questionCard {
flex: none;
+41 -4
View File
@@ -272,8 +272,20 @@ function useLocalHead(status: AgentTask["status"] | null, root: string | null):
return local ? head : null;
}
/** The task's terminal, at the end of its status line. */
function StatusActions({ task }: { task: AgentTask }) {
/** The task's terminal and changes, at the end of its status line. */
function StatusActions({
task,
changes,
changesOpen,
onToggleChanges,
}: {
task: AgentTask;
changes: AgentChange[] | null;
changesOpen: boolean;
onToggleChanges: () => void;
}) {
const tx = useTx();
const count = changes?.length ?? 0;
return (
<span className={styles.statusActions}>
<IconButton
@@ -282,6 +294,18 @@ function StatusActions({ task }: { task: AgentTask }) {
title="Terminal"
onClick={() => void executeAction("terminal.openAt", task.cwd)}
/>
<button
type="button"
className={styles.changesToggle}
aria-pressed={changesOpen}
disabled={count === 0 && !changesOpen}
aria-label={changesOpen ? tx("Hide changes") : tx("Show changes")}
title={changesOpen ? tx("Hide changes") : tx("Show changes")}
onClick={onToggleChanges}
>
<Icon name="toolwindow-changes" size={14} />
{count > 0 && <span>{count}</span>}
</button>
</span>
);
}
@@ -291,10 +315,16 @@ function SessionStatus({
task,
entries,
head,
changes,
changesOpen,
onToggleChanges,
}: {
task: AgentTask;
entries: AgentEntry[] | null;
head: string | null;
changes: AgentChange[] | null;
changesOpen: boolean;
onToggleChanges: () => void;
}) {
const tx = useTx();
const project = useProject((s) => s.project);
@@ -327,7 +357,7 @@ function SessionStatus({
{task.status === "limited" && task.resetsMs != null && ` · ${tx`resets ${formatSentAt(task.resetsMs)}`}`}
</span>
<Meters task={task} entries={entries} />
<StatusActions task={task} />
<StatusActions task={task} changes={changes} changesOpen={changesOpen} onToggleChanges={onToggleChanges} />
</div>
);
}
@@ -2529,7 +2559,14 @@ function SessionComposer({
);
return (
<div className={styles.composerArea}>
<SessionStatus task={task} entries={entries} head={head} />
<SessionStatus
task={task}
entries={entries}
head={head}
changes={changes}
changesOpen={changesOpen}
onToggleChanges={onToggleChanges}
/>
{task.status === "archived" && (
<div className={styles.banner}>
<span className={styles.bannerText}>
+2 -2
View File
@@ -1124,14 +1124,14 @@ describe("session state across tasks and hosts", () => {
useAgents.setState({ docked: "A", sessionLocation: "toolWindow" });
render(<AgentsView />);
await screen.findByText("Answer A");
await screen.findByRole("button", { name: /1 changed file/ });
await waitFor(() => expect(screen.getByRole("button", { name: "Show changes" })).toBeEnabled());
expect(screen.queryByRole("complementary", { name: "Changes" })).toBeNull();
const markVisible = vi.spyOn(await import("./store"), "markVisible");
vi.spyOn(document, "hasFocus").mockReturnValue(true);
const message = screen.getByRole("textbox", { name: "Message" });
fireEvent.change(message, { target: { value: "Draft to keep" } });
const conversation = stream();
fireEvent.click(screen.getByRole("button", { name: /1 changed file/ }));
fireEvent.click(screen.getByRole("button", { name: "Show changes" }));
expect(screen.getByRole("complementary", { name: "Changes" })).toBeVisible();
expect(screen.getByText("Answer A")).not.toBeVisible();
expect(screen.queryByRole("textbox", { name: "Message" })).toBeNull();
+1
View File
@@ -2016,6 +2016,7 @@
"Show Commit in Git Log": "Mostrar confirmación en el registro de Git",
"Show Container Log": "Mostrar registro del contenedor",
"Show Context Actions": "Mostrar acciones de contexto",
"Show changes": "Mostrar cambios",
"Show Coverage": "Mostrar cobertura",
"Show Debug Tool Window": "Mostrar ventana de herramientas de depuración",
"Show Diff": "Mostrar diferencias",
+1
View File
@@ -1928,6 +1928,7 @@
"Show Commit in Git Log": "Afficher le commit dans le journal Git",
"Show Container Log": "Afficher le journal du conteneur",
"Show Context Actions": "Afficher les actions contextuelles",
"Show changes": "Afficher les modifications",
"Show Coverage": "Afficher la couverture",
"Show Debug Tool Window": "Afficher la fenêtre d'outil Débogage",
"Show Diff": "Afficher le diff",
+1
View File
@@ -2072,6 +2072,7 @@
"Show Commit in Git Log": "Git 로그에서 커밋 표시",
"Show Container Log": "컨테이너 로그 표시",
"Show Context Actions": "컨텍스트 액션 표시",
"Show changes": "변경 사항 표시",
"Show Coverage": "커버리지 표시",
"Show Debug Tool Window": "디버그 도구 창 표시",
"Show Diff": "Diff 표시",
+1
View File
@@ -2003,6 +2003,7 @@
"Show Commit in Git Log": "Pokaż zatwierdzenie w dzienniku Git",
"Show Container Log": "Pokaż dziennik kontenera",
"Show Context Actions": "Pokaż akcje kontekstowe",
"Show changes": "Pokaż zmiany",
"Show Coverage": "Pokaż pokrycie kodu",
"Show Debug Tool Window": "Pokaż okno narzędziowe debugowania",
"Show Diff": "Pokaż różnice (Diff)",
+1
View File
@@ -2004,6 +2004,7 @@
"Show Commit in Git Log": "Mostrar commit no log do Git",
"Show Container Log": "Mostrar log do contêiner",
"Show Context Actions": "Mostrar ações de contexto",
"Show changes": "Mostrar alterações",
"Show Coverage": "Mostrar cobertura",
"Show Debug Tool Window": "Mostrar janela de ferramentas de depuração",
"Show Diff": "Mostrar diff",
+1
View File
@@ -2364,6 +2364,7 @@
"Show Commit in Git Log": "在 Git 日志中显示提交",
"Show Container Log": "显示容器日志",
"Show Context Actions": "显示上下文操作",
"Show changes": "显示更改",
"Show Coverage": "显示覆盖率",
"Show Debug Tool Window": "显示调试工具窗口",
"Show Diff": "显示差异",