summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
authorNolan Darilek <[email protected]>2026-01-22 05:10:53 -0600
committerGitHub <[email protected]>2026-01-22 05:10:53 -0600
commit3435327bc074a7ba8c3fe8939c97de54bbdefd65 (patch)
treed906b07c16e8e90808e5f78b72f72e8737f53eeb /packages/app/src
parent8a043edfd5a504421301cc183eedf4ecdb4c57b0 (diff)
downloadopencode-3435327bc074a7ba8c3fe8939c97de54bbdefd65.tar.gz
opencode-3435327bc074a7ba8c3fe8939c97de54bbdefd65.zip
fix(app): session screen accessibility improvements (#9907)
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/components/dialog-connect-provider.tsx2
-rw-r--r--packages/app/src/components/dialog-edit-project.tsx2
-rw-r--r--packages/app/src/components/dialog-select-model.tsx14
-rw-r--r--packages/app/src/components/dialog-select-server.tsx1
-rw-r--r--packages/app/src/components/prompt-input.tsx50
-rw-r--r--packages/app/src/components/session-context-usage.tsx2
-rw-r--r--packages/app/src/components/session/session-header.tsx55
-rw-r--r--packages/app/src/components/session/session-sortable-tab.tsx2
-rw-r--r--packages/app/src/components/session/session-sortable-terminal-tab.tsx1
-rw-r--r--packages/app/src/pages/layout.tsx11
-rw-r--r--packages/app/src/pages/session.tsx15
11 files changed, 96 insertions, 59 deletions
diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx
index 4ec4c8daa..d8d4ad9c2 100644
--- a/packages/app/src/components/dialog-connect-provider.tsx
+++ b/packages/app/src/components/dialog-connect-provider.tsx
@@ -143,7 +143,7 @@ export function DialogConnectProvider(props: { provider: string }) {
}
return (
- <Dialog title={<IconButton tabIndex={-1} icon="arrow-left" variant="ghost" onClick={goBack} />}>
+ <Dialog title={<IconButton tabIndex={-1} icon="arrow-left" variant="ghost" onClick={goBack} aria-label="Go back" />}>
<div class="flex flex-col gap-6 px-2.5 pb-3">
<div class="px-2.5 flex gap-4 items-center">
<ProviderIcon id={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" />
diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx
index acf146ef5..cf5535a67 100644
--- a/packages/app/src/components/dialog-edit-project.tsx
+++ b/packages/app/src/components/dialog-edit-project.tsx
@@ -193,6 +193,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
{(color) => (
<button
type="button"
+ aria-label={`Select ${color} color`}
+ aria-pressed={store.color === color}
classList={{
"flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true,
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover":
diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx
index 4fd8af1b3..5569d7780 100644
--- a/packages/app/src/components/dialog-select-model.tsx
+++ b/packages/app/src/components/dialog-select-model.tsx
@@ -1,5 +1,5 @@
import { Popover as Kobalte } from "@kobalte/core/popover"
-import { Component, createMemo, createSignal, JSX, Show } from "solid-js"
+import { Component, ComponentProps, createMemo, createSignal, JSX, Show, ValidComponent } from "solid-js"
import { useLocal } from "@/context/local"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { popularProviders } from "@/hooks/use-providers"
@@ -86,10 +86,12 @@ const ModelList: Component<{
)
}
-export const ModelSelectorPopover: Component<{
+export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
provider?: string
- children: JSX.Element
-}> = (props) => {
+ children?: JSX.Element
+ triggerAs?: T
+ triggerProps?: ComponentProps<T>
+}) {
const [open, setOpen] = createSignal(false)
const dialog = useDialog()
@@ -101,7 +103,9 @@ export const ModelSelectorPopover: Component<{
return (
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
- <Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
+ <Kobalte.Trigger as={props.triggerAs ?? "div"} {...(props.triggerProps as any)}>
+ {props.children}
+ </Kobalte.Trigger>
<Kobalte.Portal>
<Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx
index bb0ad5b43..a8e7ca4f7 100644
--- a/packages/app/src/components/dialog-select-server.tsx
+++ b/packages/app/src/components/dialog-select-server.tsx
@@ -158,6 +158,7 @@ export function DialogSelectServer() {
icon="circle-x"
variant="ghost"
class="bg-transparent transition-opacity shrink-0 hover:scale-110"
+ aria-label="Remove server"
onClick={(e) => {
e.stopPropagation()
handleRemove(i)
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index 3bd7856ee..825f2b116 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -1487,6 +1487,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
variant="ghost"
class="h-6 w-6"
onClick={() => prompt.context.removeActive()}
+ aria-label="Remove active file from context"
/>
</div>
)}
@@ -1524,6 +1525,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
variant="ghost"
class="h-6 w-6"
onClick={() => prompt.context.remove(item.key)}
+ aria-label="Remove file from context"
/>
</div>
)}
@@ -1556,6 +1558,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
type="button"
onClick={() => removeImageAttachment(attachment.id)}
class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-stronger-non-alpha border border-border-base flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-surface-raised-base-hover"
+ aria-label="Remove attachment"
>
<Icon name="close" class="size-3 text-text-weak" />
</button>
@@ -1574,6 +1577,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
editorRef = el
props.ref?.(el)
}}
+ role="textbox"
+ aria-multiline="true"
+ aria-label={
+ store.mode === "shell"
+ ? language.t("prompt.placeholder.shell")
+ : language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })
+ }
contenteditable="true"
onInput={handleInput}
onPaste={handlePaste}
@@ -1638,21 +1648,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</TooltipKeybind>
}
>
- <ModelSelectorPopover>
- <TooltipKeybind
- placement="top"
- title={language.t("command.model.choose")}
- keybind={command.keybind("model.choose")}
+ <TooltipKeybind
+ placement="top"
+ title={language.t("command.model.choose")}
+ keybind={command.keybind("model.choose")}
+ >
+ <ModelSelectorPopover
+ triggerAs={Button}
+ triggerProps={{ variant: "ghost" }}
>
- <Button as="div" variant="ghost">
- <Show when={local.model.current()?.provider?.id}>
- <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
- </Show>
- {local.model.current()?.name ?? language.t("dialog.model.select.title")}
- <Icon name="chevron-down" size="small" />
- </Button>
- </TooltipKeybind>
- </ModelSelectorPopover>
+ <Show when={local.model.current()?.provider?.id}>
+ <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
+ </Show>
+ {local.model.current()?.name ?? language.t("dialog.model.select.title")}
+ <Icon name="chevron-down" size="small" />
+ </ModelSelectorPopover>
+ </TooltipKeybind>
</Show>
<Show when={local.model.variant.list().length > 0}>
<TooltipKeybind
@@ -1683,6 +1694,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
"text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory),
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
}}
+ aria-label="Toggle auto-accept permissions"
+ aria-pressed={permission.isAutoAccepting(params.id!, sdk.directory)}
>
<Icon
name="chevron-double-right"
@@ -1711,7 +1724,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<SessionContextUsage />
<Show when={store.mode === "normal"}>
<Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
- <Button type="button" variant="ghost" class="size-6" onClick={() => fileInputRef.click()}>
+ <Button
+ type="button"
+ variant="ghost"
+ class="size-6"
+ onClick={() => fileInputRef.click()}
+ aria-label="Attach file"
+ >
<Icon name="photo" class="size-4.5" />
</Button>
</Tooltip>
@@ -1743,6 +1762,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
icon={working() ? "stop" : "arrow-up"}
variant="primary"
class="h-6 w-4.5"
+ aria-label={working() ? "Stop" : "Send message"}
/>
</Tooltip>
</div>
diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx
index 53148d416..677e29dbb 100644
--- a/packages/app/src/components/session-context-usage.tsx
+++ b/packages/app/src/components/session-context-usage.tsx
@@ -96,7 +96,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
<Switch>
<Match when={variant() === "indicator"}>{circle()}</Match>
<Match when={true}>
- <Button type="button" variant="ghost" class="size-6" onClick={openContext}>
+ <Button type="button" variant="ghost" class="size-6" onClick={openContext} aria-label="View context usage">
{circle()}
</Button>
</Match>
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx
index 98b9635c5..f2ffa3ec5 100644
--- a/packages/app/src/components/session/session-header.tsx
+++ b/packages/app/src/components/session/session-header.tsx
@@ -135,6 +135,7 @@ export function SessionHeader() {
type="button"
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
onClick={() => command.trigger("file.open")}
+ aria-label="Search files"
>
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-visible">
<Icon name="magnifying-glass" size="normal" class="icon-base shrink-0" />
@@ -184,6 +185,10 @@ export function SessionHeader() {
variant="ghost"
class="group/review-toggle size-6 p-0"
onClick={() => view().reviewPanel.toggle()}
+ aria-label="Toggle review panel"
+ aria-expanded={view().reviewPanel.opened()}
+ aria-controls="review-panel"
+ tabIndex={showReview() ? 0 : -1}
>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
@@ -214,6 +219,9 @@ export function SessionHeader() {
variant="ghost"
class="group/terminal-toggle size-6 p-0"
onClick={() => view().terminal.toggle()}
+ aria-label="Toggle terminal"
+ aria-expanded={view().terminal.opened()}
+ aria-controls="terminal-panel"
>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
@@ -235,32 +243,23 @@ export function SessionHeader() {
</Button>
</TooltipKeybind>
</div>
- <div
- class="flex items-center"
- classList={{
- "opacity-0 pointer-events-none": !showShare(),
- }}
- aria-hidden={!showShare()}
- >
- <Popover
- title={language.t("session.share.popover.title")}
- description={
- shareUrl()
- ? language.t("session.share.popover.description.shared")
- : language.t("session.share.popover.description.unshared")
- }
- trigger={
- <Tooltip class="shrink-0" value={language.t("command.session.share")}>
- <Button
- variant="secondary"
- classList={{ "rounded-r-none": shareUrl() !== undefined }}
- style={{ scale: 1 }}
- >
- {language.t("session.share.action.share")}
- </Button>
- </Tooltip>
- }
- >
+ <Show when={showShare()}>
+ <div class="flex items-center">
+ <Popover
+ title={language.t("session.share.popover.title")}
+ description={
+ shareUrl()
+ ? language.t("session.share.popover.description.shared")
+ : language.t("session.share.popover.description.unshared")
+ }
+ triggerAs={Button}
+ triggerProps={{
+ variant: "secondary",
+ classList: { "rounded-r-none": shareUrl() !== undefined },
+ style: { scale: 1 },
+ }}
+ trigger={language.t("session.share.action.share")}
+ >
<div class="flex flex-col gap-2">
<Show
when={shareUrl()}
@@ -322,10 +321,12 @@ export function SessionHeader() {
class="rounded-l-none"
onClick={copyLink}
disabled={state.unshare}
+ aria-label="Copy share link"
/>
</Tooltip>
</Show>
- </div>
+ </div>
+ </Show>
</div>
</Portal>
)}
diff --git a/packages/app/src/components/session/session-sortable-tab.tsx b/packages/app/src/components/session/session-sortable-tab.tsx
index a4a434b05..6f2c044fc 100644
--- a/packages/app/src/components/session/session-sortable-tab.tsx
+++ b/packages/app/src/components/session/session-sortable-tab.tsx
@@ -37,7 +37,7 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
value={props.tab}
closeButton={
<Tooltip value={language.t("common.closeTab")} placement="bottom">
- <IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} />
+ <IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} aria-label="Close tab" />
</Tooltip>
}
hideCloseButton
diff --git a/packages/app/src/components/session/session-sortable-terminal-tab.tsx b/packages/app/src/components/session/session-sortable-terminal-tab.tsx
index a3aaf6061..3ef395c9a 100644
--- a/packages/app/src/components/session/session-sortable-terminal-tab.tsx
+++ b/packages/app/src/components/session/session-sortable-terminal-tab.tsx
@@ -139,6 +139,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
e.stopPropagation()
close()
}}
+ aria-label="Close terminal"
/>
}
>
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index befdf721d..2a050543c 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -1916,6 +1916,7 @@ export default function Layout(props: ParentProps) {
"bg-surface-base-hover border border-border-weak-base": !selected() && open(),
}}
onClick={() => navigateToProject(props.project.worktree)}
+ onBlur={() => setOpen(false)}
>
<ProjectIcon project={props.project} notify />
</button>
@@ -2343,7 +2344,8 @@ export default function Layout(props: ParentProps) {
<div class="relative bg-background-base flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
<Titlebar />
<div class="flex-1 min-h-0 flex">
- <div
+ <nav
+ aria-label="Projects and sessions"
classList={{
"hidden xl:block": true,
"relative shrink-0": true,
@@ -2364,7 +2366,7 @@ export default function Layout(props: ParentProps) {
onCollapse={layout.sidebar.close}
/>
</Show>
- </div>
+ </nav>
<div class="xl:hidden">
<div
classList={{
@@ -2376,7 +2378,8 @@ export default function Layout(props: ParentProps) {
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
}}
/>
- <div
+ <nav
+ aria-label="Projects and sessions"
classList={{
"@container fixed top-10 bottom-0 left-0 z-50 w-72 bg-background-base transition-transform duration-200 ease-out": true,
"translate-x-0": layout.mobileSidebar.opened(),
@@ -2385,7 +2388,7 @@ export default function Layout(props: ParentProps) {
onClick={(e) => e.stopPropagation()}
>
<SidebarContent mobile />
- </div>
+ </nav>
</div>
<main
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 5e4eeafd5..19ee5d304 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -782,7 +782,7 @@ export default function Page() {
const activeElement = document.activeElement as HTMLElement | undefined
if (activeElement) {
const isProtected = activeElement.closest("[data-prevent-autofocus]")
- const isInput = /^(INPUT|TEXTAREA|SELECT)$/.test(activeElement.tagName) || activeElement.isContentEditable
+ const isInput = /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(activeElement.tagName) || activeElement.isContentEditable
if (isProtected || isInput) return
}
if (dialog.active) return
@@ -1404,6 +1404,7 @@ export default function Page() {
<div
ref={autoScroll.contentRef}
+ role="log"
class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
classList={{
"w-full": true,
@@ -1552,7 +1553,7 @@ export default function Page() {
{/* Desktop tabs panel (Review + Context + Files) - hidden on mobile */}
<Show when={isDesktop() && showTabs()}>
- <div class="relative flex-1 min-w-0 h-full border-l border-border-weak-base">
+ <aside id="review-panel" aria-label="Review and files" class="relative flex-1 min-w-0 h-full border-l border-border-weak-base">
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
@@ -1586,7 +1587,7 @@ export default function Page() {
value="context"
closeButton={
<Tooltip value={language.t("common.closeTab")} placement="bottom">
- <IconButton icon="close" variant="ghost" onClick={() => tabs().close("context")} />
+ <IconButton icon="close" variant="ghost" onClick={() => tabs().close("context")} aria-label="Close context tab" />
</Tooltip>
}
hideCloseButton
@@ -1612,6 +1613,7 @@ export default function Page() {
variant="ghost"
iconSize="large"
onClick={() => dialog.show(() => <DialogSelectFile />)}
+ aria-label="Open file"
/>
</TooltipKeybind>
</div>
@@ -1913,12 +1915,15 @@ export default function Page() {
</Show>
</DragOverlay>
</DragDropProvider>
- </div>
+ </aside>
</Show>
</div>
<Show when={isDesktop() && view().terminal.opened()}>
<div
+ id="terminal-panel"
+ role="region"
+ aria-label="Terminal"
class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
style={{ height: `${layout.terminal.height()}px` }}
>
@@ -1990,7 +1995,7 @@ export default function Page() {
keybind={command.keybind("terminal.new")}
class="flex items-center"
>
- <IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} />
+ <IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} aria-label="New terminal" />
</TooltipKeybind>
</div>
</Tabs.List>