Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 46 additions & 31 deletions packages/app/src/components/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
import { useComments } from "@/context/comments"
import { Button } from "@opencode-ai/ui/button"
import { ButtonShortcut } from "@opencode-ai/ui/button-shortcut"
import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
import { Icon } from "@opencode-ai/ui/icon"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
Expand Down Expand Up @@ -1450,42 +1451,41 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Show when={store.mode === "normal" || store.mode === "shell"}>
<DockTray attach="top">
<div class="px-1.75 pt-5.5 pb-2 flex items-center gap-2 min-w-0">
<div class="flex items-center gap-1.5 min-w-0 flex-1 relative">
<div class="flex h-7 items-center gap-1.5 min-w-0 flex-1 relative">
<div
class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0"
class="flex items-center max-w-[160px] min-w-0 absolute inset-y-0 left-0"
style={{
padding: "0 4px 0 8px",
padding: "0 8px",
...shell(),
}}
>
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
<div class="size-4 shrink-0" />
<span class="truncate text-13-regular text-text-strong">{language.t("prompt.mode.shell")}</span>
</div>
<div class="flex items-center gap-1.5 min-w-0 flex-1">
<div data-component="prompt-agent-control">
<TooltipKeybind
placement="top"
gutter={4}
title={language.t("command.agent.cycle")}
keybind={command.keybind("agent.cycle")}
>
<Select
size="normal"
options={agentNames()}
current={local.agent.current()?.name ?? ""}
onSelect={(value) => {
local.agent.set(value)
restoreFocus()
}}
class="capitalize max-w-[160px] text-text-base"
valueClass="truncate text-13-regular text-text-base"
triggerStyle={control()}
triggerProps={{ "data-action": "prompt-agent" }}
variant="ghost"
/>
</TooltipKeybind>
</div>
<Show when={store.mode !== "shell"}>
<Show when={store.mode !== "shell"}>
<div class="flex items-center gap-1.5 min-w-0 flex-1">
<div data-component="prompt-agent-control">
<TooltipKeybind
placement="top"
gutter={4}
title={language.t("command.agent.cycle")}
keybind={command.keybind("agent.cycle")}
>
<Select
size="normal"
options={agentNames()}
current={local.agent.current()?.name ?? ""}
onSelect={(value) => {
local.agent.set(value)
restoreFocus()
}}
class="capitalize max-w-[160px] text-text-base"
valueClass="truncate text-13-regular text-text-base"
triggerStyle={control()}
triggerProps={{ "data-action": "prompt-agent" }}
variant="ghost"
/>
</TooltipKeybind>
</div>
<div data-component="prompt-model-control">
<Show
when={providers.paid().length > 0}
Expand Down Expand Up @@ -1581,7 +1581,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
/>
</TooltipKeybind>
</div>
</Show>
</div>
</Show>
<div class="absolute inset-y-0 right-0 flex items-center" style={shell()}>
<ButtonShortcut
type="button"
variant="ghost"
size="small"
shortcut="Esc"
shortcutAria="Escape"
class="h-6 gap-2 rounded-[6px] border-none px-0 py-0 pl-3 pr-0.75 text-13-medium text-text-base shadow-none"
tabIndex={store.mode === "shell" ? undefined : -1}
onClick={() => setMode("normal")}
aria-label={language.t("common.cancel")}
>
{language.t("common.cancel")}
</ButtonShortcut>
</div>
</div>
</div>
Expand Down
36 changes: 36 additions & 0 deletions packages/ui/src/components/button-shortcut.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[data-component="button"][data-button-shortcut] {
[data-slot="button-shortcut-label"] {
min-width: 0;
}

[data-slot="button-shortcut-key"] {
display: flex;
align-items: center;
flex-shrink: 0;
}

[data-slot="button-shortcut-key"] [data-component="keybind"] {
box-shadow: none;
background: var(--surface-raised-base);
color: var(--text-weak);
font-family: var(--font-family-sans);
font-weight: var(--font-weight-regular);
}

&[data-size="small"] [data-slot="button-shortcut-key"] [data-component="keybind"],
&[data-size="normal"] [data-slot="button-shortcut-key"] [data-component="keybind"] {
height: 18px;
padding: 0 4px;
border-radius: 3px;
font-size: var(--font-size-small);
line-height: 18px;
}

&[data-size="large"] [data-slot="button-shortcut-key"] [data-component="keybind"] {
height: 20px;
padding: 0 6px;
border-radius: 4px;
font-size: var(--font-size-small);
line-height: 20px;
}
}
86 changes: 86 additions & 0 deletions packages/ui/src/components/button-shortcut.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// @ts-nocheck
import { ButtonShortcut } from "./button-shortcut"

const docs = `### Overview
Button with a trailing shortcut keycap.

Use this when the action label and shortcut should be taught together at the control level.

### API
- Inherits Button props.
- \`shortcut\`: visible keycap text.
- \`shortcutAria\`: semantic shortcut string for \`aria-keyshortcuts\`.
- \`shortcutClass\`: optional class override for the keycap.

### Variants and states
- Uses the same \`variant\` and \`size\` options as \`Button\`.
- Supports disabled state.

### Accessibility
- Keep the visible shortcut concise.
- Use \`shortcutAria\` for the canonical key sequence when it differs from the visible label.

### Theming/tokens
- Extends \`Button\` and composes \`Keybind\`.

`

export default {
title: "UI/ButtonShortcut",
id: "components-button-shortcut",
component: ButtonShortcut,
tags: ["autodocs"],
parameters: {
docs: {
description: {
component: docs,
},
},
},
args: {
children: "Cancel",
shortcut: "Esc",
shortcutAria: "Escape",
variant: "ghost",
size: "small",
},
argTypes: {
variant: {
control: "select",
options: ["primary", "secondary", "ghost"],
},
size: {
control: "select",
options: ["small", "normal", "large"],
},
},
}

export const Basic = {}

export const Sizes = {
render: () => (
<div style={{ display: "flex", gap: "12px", "align-items": "center" }}>
<ButtonShortcut size="small" variant="ghost" shortcut="Esc" shortcutAria="Escape">
Cancel
</ButtonShortcut>
<ButtonShortcut size="normal" variant="secondary" shortcut="Tab" shortcutAria="Tab">
Focus
</ButtonShortcut>
<ButtonShortcut size="large" variant="primary" shortcut="Enter" shortcutAria="Enter">
Submit
</ButtonShortcut>
</div>
),
}

export const Shell = {
args: {
children: "Cancel",
shortcut: "Esc",
shortcutAria: "Escape",
variant: "ghost",
size: "small",
class: "h-6 gap-2 rounded-[6px] border-none px-0 py-0 pl-3 pr-0.75 text-13-medium text-text-base shadow-none",
},
}
33 changes: 33 additions & 0 deletions packages/ui/src/components/button-shortcut.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { type ComponentProps, Show, splitProps } from "solid-js"
import { Button, type ButtonProps } from "./button"
import { Keybind } from "./keybind"

export interface ButtonShortcutProps extends ButtonProps {
shortcut?: string
shortcutAria?: string
shortcutClass?: string
shortcutClassList?: ComponentProps<"span">["classList"]
}

export function ButtonShortcut(props: ButtonShortcutProps) {
const [split, rest] = splitProps(props, [
"children",
"shortcut",
"shortcutAria",
"shortcutClass",
"shortcutClassList",
])

return (
<Button {...rest} aria-keyshortcuts={split.shortcutAria} data-button-shortcut={split.shortcut ? "true" : undefined}>
<span data-slot="button-shortcut-label">{split.children}</span>
<Show when={split.shortcut}>
<span data-slot="button-shortcut-key">
<Keybind class={split.shortcutClass} classList={split.shortcutClassList}>
{split.shortcut}
</Keybind>
</span>
</Show>
</Button>
)
}
1 change: 1 addition & 0 deletions packages/ui/src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@import "../components/avatar.css" layer(components);
@import "../components/basic-tool.css" layer(components);
@import "../components/button.css" layer(components);
@import "../components/button-shortcut.css" layer(components);
@import "../components/card.css" layer(components);
@import "../components/tool-error-card.css" layer(components);
@import "../components/checkbox.css" layer(components);
Expand Down
Loading