diff --git a/src/components/WorkspaceControls.tsx b/src/components/WorkspaceControls.tsx
index 07502c7db..04b3d1763 100644
--- a/src/components/WorkspaceControls.tsx
+++ b/src/components/WorkspaceControls.tsx
@@ -1,5 +1,6 @@
import { store, toggleSettingsDialog, toggleSidebar } from '../store/store';
import { mod } from '../lib/platform';
+import { ChevronLeftAltIcon, GearIcon } from './icons';
import { IconButton } from './IconButton';
import { UpdateButton } from './UpdateButton';
@@ -13,25 +14,15 @@ export function WorkspaceControls() {
>
-
-
+ />
}
onClick={() => toggleSidebar()}
title={`${store.sidebarVisible ? 'Collapse' : 'Show'} sidebar (${mod}+B)`}
/>
-
-
- }
+ icon={}
onClick={() => toggleSettingsDialog(true)}
title={`Settings (${mod}+,)`}
/>
diff --git a/src/components/icons.client.test.tsx b/src/components/icons.client.test.tsx
new file mode 100644
index 000000000..524df49cd
--- /dev/null
+++ b/src/components/icons.client.test.tsx
@@ -0,0 +1,71 @@
+import { render } from 'solid-js/web';
+import { afterEach, describe, expect, it } from 'vitest';
+import * as icons from './icons';
+
+const disposers: Array<() => void> = [];
+
+afterEach(() => {
+ while (disposers.length > 0) disposers.pop()?.();
+ document.body.replaceChildren();
+});
+
+/** Every exported icon component, so the whole module stays render-covered. */
+const ALL_ICONS = Object.values(icons);
+
+describe('shared icon module', () => {
+ it('renders every icon at its default size of 16 and hides it from the a11y tree', () => {
+ for (const Icon of ALL_ICONS) {
+ const container = document.createElement('div');
+ document.body.append(container);
+ disposers.push(render(() => , container));
+
+ const svg = container.querySelector('svg');
+ expect(svg, Icon.name).not.toBeNull();
+ expect(svg?.getAttribute('width'), Icon.name).toBe('16');
+ expect(svg?.getAttribute('height'), Icon.name).toBe('16');
+ expect(svg?.getAttribute('aria-hidden'), Icon.name).toBe('true');
+ expect(svg?.getAttribute('role'), Icon.name).toBeNull();
+ expect(svg?.querySelector('title'), Icon.name).toBeNull();
+ }
+ });
+
+ it('honors a custom size (12, the small size from issue #208)', () => {
+ for (const Icon of ALL_ICONS) {
+ const container = document.createElement('div');
+ document.body.append(container);
+ disposers.push(render(() => , container));
+
+ const svg = container.querySelector('svg');
+ expect(svg?.getAttribute('width'), Icon.name).toBe('12');
+ expect(svg?.getAttribute('height'), Icon.name).toBe('12');
+ }
+ });
+
+ it('keeps the monochrome currentColor contract', () => {
+ for (const Icon of ALL_ICONS) {
+ const container = document.createElement('div');
+ document.body.append(container);
+ disposers.push(render(() => , container));
+
+ const svg = container.querySelector('svg');
+ const fill = svg?.getAttribute('fill');
+ expect(['currentColor', 'none'], Icon.name).toContain(fill);
+ if (fill === 'none') {
+ expect(svg?.getAttribute('stroke'), Icon.name).toBe('currentColor');
+ }
+ }
+ });
+
+ it('renders a title with role="img" when a title is provided', () => {
+ for (const Icon of ALL_ICONS) {
+ const container = document.createElement('div');
+ document.body.append(container);
+ disposers.push(render(() => , container));
+
+ const svg = container.querySelector('svg');
+ expect(svg?.getAttribute('aria-hidden'), Icon.name).toBeNull();
+ expect(svg?.getAttribute('role'), Icon.name).toBe('img');
+ expect(svg?.querySelector('title')?.textContent, Icon.name).toBe('Test label');
+ }
+ });
+});
diff --git a/src/components/icons.tsx b/src/components/icons.tsx
index e17ab3b37..1272e2d2b 100644
--- a/src/components/icons.tsx
+++ b/src/components/icons.tsx
@@ -550,3 +550,491 @@ export function CircleCheckIcon(props: IconProps): JSX.Element {
);
}
+
+interface StrokeSvgIconProps extends IconProps {
+ children: JSX.Element;
+ viewBox?: string;
+ strokeWidth: number | string;
+ roundCaps?: boolean;
+ roundJoins?: boolean;
+}
+
+/** Stroke-drawn glyphs (fill="none"): attributes mirror what the call sites pasted. */
+function StrokeSvgIcon(props: StrokeSvgIconProps): JSX.Element {
+ const size = () => props.size ?? 16;
+
+ return (
+
+ );
+}
+
+/** The parallel-bars logo from the window title bar. */
+export function LogoIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+
+
+ );
+}
+
+/** Thin 10x10 window controls (custom title bar). */
+export function MinimizeIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+export function MaximizeIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+export function RestoreIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+ );
+}
+
+export function CloseThinIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Coordinator/bot marker shown next to agent-driven tasks. */
+export function BotIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Filled chevron for collapsible section headers; see also ChevronDownIcon above. */
+export function ChevronDownAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Chunky plus for "create" affordances; PlusIcon above is the inline-text variant. */
+export function PlusLargeIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Compact plus that stays legible at 13px and below. */
+export function PlusSmallIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Single horizontal bar: the collapsed-task affordance. */
+export function MinusIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Keyboard: the shortcuts cheat-sheet affordance in the sidebar footer. */
+export function KeyboardIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+ );
+}
+
+/** Side panel with a divider: toggles the task canvas. */
+export function PanelRightIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+ );
+}
+
+/** Corners-in brackets: shown while unfocused as the enter-focus affordance in the document workspace, and always on the task title bar. */
+export function CollapseAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Check inside a circle: the Finish (merge or push) button's idle glyph. */
+export function CheckCircleIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Terminal window chrome (panel-layout toggle); see also the prompt-style TerminalIcon above. */
+export function TerminalAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+ );
+}
+
+/** Two side-by-side panels: the split-terminal layout toggle. */
+export function ColumnsIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+ );
+}
+
+/** Phone handset: remote-access affordances. */
+export function PhoneIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+ );
+}
+
+/** Crossing strokes: the Arena (agent-vs-agent) affordance. */
+export function ScrambleIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+ );
+}
+
+/** Stroke-drawn download; see also the filled DownloadIcon above. */
+export function DownloadAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+
+ );
+}
+
+/** Circular arrow: restart to install an update. */
+export function RefreshIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+ );
+}
+
+/** Oversized check for success states (phone connected). */
+export function CheckLargeIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** The GitHub octocat mark; GitHubIcon above is this project's own glyph. */
+export function GitHubMarkIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Gear: project settings — name, folder and agents. */
+export function GearIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Filled octicon chevron for commit-step navigation; see also the stroke ChevronLeftIcon above. */
+export function ChevronLeftAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Lit bulb: the thinking-tours affordance in the sidebar footer. */
+export function LightbulbIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+ );
+}
+
+/** Stroke-drawn bar: minimize the document workspace back to the sidebar. */
+export function MinimizeBarIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Filled octicon chevron for commit-step navigation; see also the stroke ChevronRightIcon above. */
+export function ChevronRightAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Sharp stroke chevron on the Advanced options disclosure; see also ChevronRightIcon above. */
+export function ChevronRightThinIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Rounded stroke chevron for Arena back buttons; see also ChevronLeftIcon above. */
+export function ChevronLeftThinIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Corners-out brackets: shown while focus mode is active, as the exit-focus affordance; pairs with CollapseAltIcon. */
+export function ExpandAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Margins with in/out arrows: the reading-width toggle in the document toolbar. */
+export function FullWidthIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Outline folder: reveal the project folder in the file manager; FolderIcon above is filled. */
+export function FolderOpenIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** The document-project glyph: sidebar rows, the workspace header and the file tree. */
+export function DocumentIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+ );
+}
+
+/** Stroke-drawn note card in the annotation margin; CommentIcon above is the speech bubble. */
+export function CommentAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+ );
+}
+
+/** Note card with lines: the composer's note mode. */
+export function NoteIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Circled question mark: the composer's ask mode. */
+export function QuestionIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Filled square: stop a running arena competitor; StopIcon above is the octagon. */
+export function StopAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Stroke-drawn trash bin: delete a history match; TrashIcon above is the filled one. */
+export function TrashAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Five-pointed star: rate an arena competitor's approach. */
+export function StarIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Cradle joining two nodes: merge a worktree into the project's main branch. */
+export function MergeIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+
+
+ );
+}
+
+/** Boxed shield: the arena project's container; ShieldIcon above is the filled badge. */
+export function ShieldAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+ );
+}
+
+/** Two ruled columns: compare every arena approach side by side. */
+export function CompareIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Chasing arrows: run the arena match again; SyncIcon above is the filled one. */
+export function SyncAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+
+
+ );
+}
+
+/** Stroke-drawn plus: start a fresh arena match. */
+export function PlusThinIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Stroke-drawn clock face: arena match history; ClockIcon above is the filled one. */
+export function ClockAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+ );
+}
+
+/** Two nodes joined by a loop: generate a tour of the changes. */
+export function CycleIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+
+
+ );
+}
+
+/** Stroke-drawn X: cancel tour generation; CloseThinIcon above is the 10x10 window control. */
+export function CloseAltIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/** Stroke-drawn arrow: the tour button's go affordance once it is idle. */
+export function ArrowRightThinIcon(props: IconProps): JSX.Element {
+ return (
+
+
+
+ );
+}
diff --git a/src/documents/AnnotationMarker.tsx b/src/documents/AnnotationMarker.tsx
index a35de063f..6c7a705bb 100644
--- a/src/documents/AnnotationMarker.tsx
+++ b/src/documents/AnnotationMarker.tsx
@@ -9,6 +9,7 @@ import {
import { AnnotationBubble } from './AnnotationBubble';
import type { DocumentAnnotation } from './types';
import { focusAnnotation, registerPinnedBubble, workspaceUi } from './workspace-ui';
+import { CommentAltIcon } from '../components/icons';
interface AnnotationMarkerProps {
/** Portalled controls must follow their owning panel’s visibility. */
@@ -172,24 +173,7 @@ export function AnnotationMarker(props: AnnotationMarkerProps) {
>
{/* A question waiting on its agent spins in the margin, so the wait is
visible without opening the bubble to read "Answering…". */}
-
-
-
-
- }
- >
+ }>
1}>
diff --git a/src/documents/BlockActions.tsx b/src/documents/BlockActions.tsx
index fe63bcc5f..a77b2841f 100644
--- a/src/documents/BlockActions.tsx
+++ b/src/documents/BlockActions.tsx
@@ -1,7 +1,17 @@
-import { For, createEffect, createSignal, onCleanup, type Accessor, type JSX } from 'solid-js';
+import {
+ For,
+ Show,
+ createEffect,
+ createSignal,
+ onCleanup,
+ type Accessor,
+ type Component,
+ type JSX,
+} from 'solid-js';
import { Portal } from 'solid-js/web';
import { createAnchorEffect } from '../lib/floating';
import type { ComposerMode } from './store';
+import { GitGraphIcon, NoteIcon, QuestionIcon } from '../components/icons';
/** Passage actions; direct editing is an explicit option in the composer. */
export type BlockActionKind = Exclude;
@@ -11,8 +21,8 @@ interface BlockAction {
title: string;
/** The key that does the same on a selected passage, in the workspace. */
shortcut?: string;
- /** 16×16 icon path. */
- path: string;
+ /** The action's glyph, the app's shared icon-module component for it. */
+ icon: Component<{ size?: number | string }>;
}
/** Labeled passage actions. */
@@ -20,32 +30,29 @@ export const BLOCK_ACTIONS: readonly BlockAction[] = [
{
kind: 'proposals',
title: 'Rewrite with AI',
- path: 'M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z',
+ icon: GitGraphIcon,
},
{
kind: 'note',
title: 'Add comment',
shortcut: 'C',
- path: 'M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25Zm1.75-.25a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25ZM3.5 6.25a.75.75 0 0 1 .75-.75h7a.75.75 0 0 1 0 1.5h-7a.75.75 0 0 1-.75-.75Zm.75 2.25h4a.75.75 0 0 1 0 1.5h-4a.75.75 0 0 1 0-1.5Z',
+ icon: NoteIcon,
},
{
kind: 'question',
title: 'Ask about this passage',
shortcut: 'A',
- path: 'M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.749.749 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4a2.756 2.756 0 0 1 1.637.525c.503.377.863.965.863 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.103-.067.207-.132.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z',
+ icon: QuestionIcon,
},
];
-function iconPath(kind: BlockActionKind): string {
- return BLOCK_ACTIONS.find((a) => a.kind === kind)?.path ?? '';
-}
-
/** One action's glyph, shared by the block toolbar and composer. */
export function ActionIcon(props: { kind: BlockActionKind }) {
+ const icon = () => BLOCK_ACTIONS.find((a) => a.kind === props.kind)?.icon;
return (
-
+
+ {(Icon) => }
+
);
}
diff --git a/src/documents/DocumentIcon.tsx b/src/documents/DocumentIcon.tsx
deleted file mode 100644
index 1c4c2afb4..000000000
--- a/src/documents/DocumentIcon.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-/** The document-project glyph: shared by the sidebar row and the workspace header. */
-export function DocumentIcon(props: { size?: number }) {
- return (
-
- );
-}
diff --git a/src/documents/DocumentWorkspacePanel.tsx b/src/documents/DocumentWorkspacePanel.tsx
index d18e0a788..8799c51e2 100644
--- a/src/documents/DocumentWorkspacePanel.tsx
+++ b/src/documents/DocumentWorkspacePanel.tsx
@@ -68,11 +68,22 @@ import { MarkdownEditor } from './MarkdownEditor';
import { afterSavingMarkdown, readMarkdownDraft } from './markdown-editing';
import { ResizablePanel, type PanelChild } from '../components/ResizablePanel';
import { createRenderedBlocks } from './use-blocks';
-import { DocumentIcon } from './DocumentIcon';
import { ActionIcon } from './BlockActions';
import { Dialog } from '../components/Dialog';
import { openInEditor, revealItemInDir } from '../lib/shell';
-import { UndoIcon } from '../components/icons';
+import {
+ CollapseAltIcon,
+ DocumentIcon,
+ ExpandAltIcon,
+ ExternalLinkIcon,
+ FolderOpenIcon,
+ FullWidthIcon,
+ GearIcon,
+ GitGraphIcon,
+ MinimizeBarIcon,
+ PanelRightIcon,
+ UndoIcon,
+} from '../components/icons';
import { setDocumentFullWidth, toggleTaskFocusMode } from '../store/ui';
import { isTerminalPaneOnScreen } from '../lib/terminalPaneVisibility';
import { errMessage } from '../lib/log';
@@ -567,17 +578,7 @@ function DocumentPane(props: { project: Project; toolbarSlot: HTMLElement | unde
}
onClick={() => setDocumentFullWidth(!store.documentFullWidth)}
>
-
+
@@ -1054,7 +1055,7 @@ export function DocumentWorkspacePanel() {
-
+ {project()?.name}{(quick) => }
@@ -1082,15 +1083,7 @@ export function DocumentWorkspacePanel() {
/>
-
+
{snapshot()?.branch ?? 'detached'} ·{' '}
{snapshot()?.headSha?.slice(0, 7) ?? 'no commits'}
@@ -1110,9 +1103,7 @@ export function DocumentWorkspacePanel() {
disabled={!editorCommand() || !openPath()}
onClick={() => void openDocumentInEditor()}
>
-
+
{/* The sidebar opens a document project as this workspace, so its
settings are reachable only from here. */}
@@ -1136,9 +1125,7 @@ export function DocumentWorkspacePanel() {
disabled={!project()}
onClick={() => setEditing(project() ?? null)}
>
-
+
-
+ {/* Unfocused shows the corners-in glyph (enter focus); focused shows corners-out. */}
+ }>
+
+
{/* Minimize, not close: the project stays open in the sidebar and the
workspace reopens from there, while an X on a task means closing it
@@ -1199,17 +1168,7 @@ export function DocumentWorkspacePanel() {
title="Minimize · reopen from the sidebar"
onClick={() => closeDocumentWorkspace()}
>
-
+
diff --git a/src/documents/FileTreePanel.tsx b/src/documents/FileTreePanel.tsx
index 061d6b4b9..706f4bc8c 100644
--- a/src/documents/FileTreePanel.tsx
+++ b/src/documents/FileTreePanel.tsx
@@ -2,7 +2,7 @@ import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
import { activeDocumentPath, documentStore, openDocumentFile } from './store';
import { loadDocumentFiles, workspaceUi } from './workspace-ui';
import { buildPathTree, type PathTreeNode } from './path-tree';
-import { DocumentIcon } from './DocumentIcon';
+import { ChevronRightAltIcon, DocumentIcon } from '../components/icons';
const DOCUMENT_RE = /\.(md|markdown|html?)$/i;
/** Mirrors MAX_FILES in electron/documents/files.ts: a list this long was cut. */
@@ -16,16 +16,10 @@ interface FileTreePanelProps {
function Chevron(props: { open: boolean }) {
return (
-
+ />
);
}
@@ -81,7 +75,7 @@ export function FileTreePanel(props: FileTreePanelProps) {
>
·}>
-
+ {nodeProps.node.name}
diff --git a/src/documents/RunComposer.draft.client.test.tsx b/src/documents/RunComposer.draft.client.test.tsx
index a1ceafeae..4b1dd59a7 100644
--- a/src/documents/RunComposer.draft.client.test.tsx
+++ b/src/documents/RunComposer.draft.client.test.tsx
@@ -1,6 +1,8 @@
import { render } from 'solid-js/web';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { setStore } from '../store/core';
+import { GitGraphIcon, NoteIcon, QuestionIcon } from '../components/icons';
+import { ActionIcon, BLOCK_ACTIONS } from './BlockActions';
import { RunComposer } from './RunComposer';
import { documentStore, setDocumentComposerDraft, type DocumentSelection } from './store';
import type { DocumentBlock } from './markdown-blocks';
@@ -39,6 +41,35 @@ const passage: DocumentSelection = {
wholeDocument: false,
};
+describe('block action glyphs', () => {
+ it('points every action at its shared icon-module component', () => {
+ expect(BLOCK_ACTIONS.find((a) => a.kind === 'proposals')?.icon).toBe(GitGraphIcon);
+ expect(BLOCK_ACTIONS.find((a) => a.kind === 'note')?.icon).toBe(NoteIcon);
+ expect(BLOCK_ACTIONS.find((a) => a.kind === 'question')?.icon).toBe(QuestionIcon);
+ });
+
+ it('renders the same glyph body as the module icon itself', () => {
+ for (const action of BLOCK_ACTIONS) {
+ const host = document.createElement('div');
+ document.body.append(host);
+ const Icon = action.icon;
+ disposers.push(
+ render(
+ () => (
+ <>
+
+
+ >
+ ),
+ host,
+ ),
+ );
+ const [fromAction, fromModule] = Array.from(host.querySelectorAll('path'));
+ expect(fromAction.getAttribute('d'), action.kind).toBe(fromModule.getAttribute('d'));
+ }
+ });
+});
+
describe('RunComposer draft mode', () => {
it('opens on the mode a hover action asked for', () => {
setDocumentComposerDraft({ text: '', mode: 'note' });
diff --git a/src/documents/RunComposer.tsx b/src/documents/RunComposer.tsx
index 31a6c57b0..509dba231 100644
--- a/src/documents/RunComposer.tsx
+++ b/src/documents/RunComposer.tsx
@@ -39,7 +39,7 @@ import type { DocumentModelChoice } from '../store/types';
import { ModelRows, type ChoiceSlot, type ModelSlot } from './ModelRows';
import { buildInteractivePrompt } from './interactive-prompt';
import { sendToDocumentAgent } from './agent-task';
-import { SendIcon, SparkleIcon } from '../components/icons';
+import { CloseIcon, SendIcon, SparkleIcon } from '../components/icons';
interface RunComposerProps {
/** The picked passage; null means the whole document. */
@@ -484,9 +484,7 @@ export function RunComposer(props: RunComposerProps) {
title="Close · Esc"
onClick={close}
>
-
+