diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index 73a7a79963e1..b2f78a99b0e0 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -12,6 +12,7 @@ export namespace Agent {
.object({
name: z.string(),
description: z.string().optional(),
+ shortDescription: z.string().optional(),
mode: z.enum(["subagent", "primary", "all"]),
builtIn: z.boolean(),
topP: z.number().optional(),
@@ -189,6 +190,7 @@ export namespace Agent {
prompt,
tools,
description,
+ short_description,
temperature,
top_p,
mode,
@@ -213,6 +215,7 @@ export namespace Agent {
...item.tools,
}
if (description) item.description = description
+ if (short_description) item.shortDescription = short_description
if (temperature != undefined) item.temperature = temperature
if (top_p != undefined) item.topP = top_p
if (mode) item.mode = mode
@@ -262,6 +265,7 @@ export namespace Agent {
model: language,
schema: z.object({
identifier: z.string(),
+ shortDescription: z.string(),
whenToUse: z.string(),
systemPrompt: z.string(),
}),
diff --git a/packages/opencode/src/agent/generate.txt b/packages/opencode/src/agent/generate.txt
index 774277b0fa8f..b4f8ca2a77dd 100644
--- a/packages/opencode/src/agent/generate.txt
+++ b/packages/opencode/src/agent/generate.txt
@@ -59,6 +59,7 @@ When a user describes what they want an agent to do, you will:
Your output must be a valid JSON object with exactly these fields:
{
"identifier": "A unique, descriptive identifier using lowercase letters, numbers, and hyphens (e.g., 'code-reviewer', 'api-docs-writer', 'test-generator')",
+"shortDescription": "A brief, human-readable summary of the agent's purpose (max 40 characters) for display in agent listings. Should be concise and descriptive, e.g., 'Reviews code for best practices and potential issues'",
"whenToUse": "A precise, actionable description starting with 'Use this agent when...' that clearly defines the triggering conditions and use cases. Ensure you include examples as described above.",
"systemPrompt": "The complete system prompt that will govern the agent's behavior, written in second person ('You are...', 'You will...') and structured for maximum clarity and effectiveness"
}
diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts
index a774c6d026be..1169c7f6bc6c 100644
--- a/packages/opencode/src/cli/cmd/agent.ts
+++ b/packages/opencode/src/cli/cmd/agent.ts
@@ -112,6 +112,7 @@ const AgentCreateCommand = cmd({
const frontmatter: any = {
description: generated.whenToUse,
+ short_description: generated.shortDescription,
mode: modeResult,
}
if (Object.keys(tools).length > 0) {
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-agent-details.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-agent-details.tsx
new file mode 100644
index 000000000000..469c09c83f6a
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-agent-details.tsx
@@ -0,0 +1,79 @@
+import { createMemo, onMount } from "solid-js"
+import { useLocal } from "@tui/context/local"
+import { useDialog } from "@tui/ui/dialog"
+import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
+import { useTheme } from "@tui/context/theme"
+import { ScrollBoxRenderable, TextAttributes } from "@opentui/core"
+import { DialogAgent } from "./dialog-agent"
+
+export function DialogAgentDetails(props: { agentName: string }) {
+ const local = useLocal()
+ const dialog = useDialog()
+ const { theme } = useTheme()
+ const dimensions = useTerminalDimensions()
+
+ const agent = createMemo(() => local.agent.list().find((a) => a.name === props.agentName))
+ const maxHeight = Math.floor(dimensions().height / 2) - 4
+
+ let scroll: ScrollBoxRenderable
+
+ onMount(() => {
+ dialog.setSize("large")
+ })
+
+ useKeyboard((evt) => {
+ if (evt.name === "escape" || evt.name === "return") {
+ evt.preventDefault()
+ dialog.replace(() => )
+ return
+ }
+ if (evt.name === "up" || (evt.ctrl && evt.name === "p")) {
+ scroll?.scrollBy(-1)
+ }
+ if (evt.name === "down" || (evt.ctrl && evt.name === "n")) {
+ scroll?.scrollBy(1)
+ }
+ if (evt.name === "pageup") {
+ scroll?.scrollBy(-10)
+ }
+ if (evt.name === "pagedown") {
+ scroll?.scrollBy(10)
+ }
+ })
+
+ return (
+
+
+
+
+ {props.agentName}
+
+ esc
+
+
+ (scroll = r)}
+ paddingLeft={4}
+ paddingRight={4}
+ paddingTop={1}
+ maxHeight={maxHeight}
+ >
+ {agent()?.shortDescription && (
+
+ {agent()?.shortDescription}
+
+ )}
+ {agent()?.description && (
+
+
+ {agent()?.description}
+
+
+ )}
+
+
+ Press esc or enter to go back
+
+
+ )
+}
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
index 65aaeb22bf98..d56a22d0eeff 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
@@ -1,18 +1,51 @@
-import { createMemo } from "solid-js"
+import { createMemo, createSignal } from "solid-js"
import { useLocal } from "@tui/context/local"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useDialog } from "@tui/ui/dialog"
+import { useTerminalDimensions } from "@opentui/solid"
+import { Keybind } from "@/util/keybind"
+import { Locale } from "@/util/locale"
+import { DialogAgentDetails } from "./dialog-agent-details"
-export function DialogAgent() {
+export function DialogAgent(props: { selectedAgent?: string }) {
const local = useLocal()
const dialog = useDialog()
+ const dimensions = useTerminalDimensions()
+
+ const [selectedAgentName, setSelectedAgentName] = createSignal(props.selectedAgent ?? local.agent.current().name)
+
+ const selectedAgentHasDescription = createMemo(() => {
+ const agent = local.agent.list().find((a) => a.name === selectedAgentName())
+ return agent && !agent.builtIn && !!agent.description
+ })
+
+ // Dialog width is min(60, terminalWidth - 2), minus ~10 for padding
+ const availableWidth = createMemo(() => Math.min(60, dimensions().width - 2) - 10)
const options = createMemo(() =>
local.agent.list().map((item) => {
+ let description: string | undefined
+
+ if (item.builtIn) {
+ description = "native"
+ } else if (item.shortDescription) {
+ description = item.shortDescription
+ } else if (item.description) {
+ description = item.description
+ }
+
+ // Truncate description based on available space after title
+ const maxDescriptionLength = availableWidth() - item.name.length - 1
+ if (description && maxDescriptionLength > 3) {
+ description = Locale.truncate(description, maxDescriptionLength)
+ } else if (description && maxDescriptionLength <= 3) {
+ description = undefined
+ }
+
return {
value: item.name,
title: item.name,
- description: item.builtIn ? "native" : item.description,
+ description,
}
}),
)
@@ -21,11 +54,25 @@ export function DialogAgent() {
setSelectedAgentName(option.value)}
onSelect={(option) => {
local.agent.set(option.value)
dialog.clear()
}}
+ keybind={[
+ {
+ keybind: Keybind.parse("ctrl+e")[0],
+ title: "show details",
+ get disabled() {
+ return !selectedAgentHasDescription()
+ },
+ onTrigger: (option) => {
+ dialog.replace(() => )
+ },
+ },
+ ]}
/>
)
}
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
index b6c5b5f8b959..55418fc57da4 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
@@ -26,6 +26,7 @@ export interface DialogSelectProps {
onTrigger: (option: DialogSelectOption) => void
}[]
current?: T
+ defaultSelected?: T
}
export interface DialogSelectOption {
@@ -53,10 +54,11 @@ export function DialogSelect(props: DialogSelectProps) {
})
createEffect(() => {
- if (props.current) {
- const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, props.current))
- if (currentIndex >= 0) {
- setStore("selected", currentIndex)
+ const initialValue = props.defaultSelected ?? props.current
+ if (initialValue) {
+ const initialIndex = flat().findIndex((opt) => isDeepEqual(opt.value, initialValue))
+ if (initialIndex >= 0) {
+ setStore("selected", initialIndex)
}
}
})
@@ -101,10 +103,13 @@ export function DialogSelect(props: DialogSelectProps) {
store.filter
if (store.filter.length > 0) {
setStore("selected", 0)
- } else if (props.current) {
- const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, props.current))
- if (currentIndex >= 0) {
- setStore("selected", currentIndex)
+ } else {
+ const initialValue = props.defaultSelected ?? props.current
+ if (initialValue) {
+ const initialIndex = flat().findIndex((opt) => isDeepEqual(opt.value, initialValue))
+ if (initialIndex >= 0) {
+ setStore("selected", initialIndex)
+ }
}
}
scroll.scrollTo(0)
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx
index 9b773111c355..843232175d6d 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx
@@ -57,7 +57,7 @@ function init() {
})
useKeyboard((evt) => {
- if (evt.name === "escape" && store.stack.length > 0) {
+ if (evt.name === "escape" && store.stack.length > 0 && !evt.defaultPrevented) {
const current = store.stack.at(-1)!
current.onClose?.()
setStore("stack", store.stack.slice(0, -1))
@@ -70,8 +70,14 @@ function init() {
let focus: Renderable | null
function refocus() {
setTimeout(() => {
+ // A new dialog was opened in the meantime (e.g., navigating between dialogs)
+ if (store.stack.length > 0) return
+
if (!focus) return
- if (focus.isDestroyed) return
+ if (focus.isDestroyed) {
+ focus = null
+ return
+ }
function find(item: Renderable) {
for (const child of item.getChildren()) {
if (child === focus) return true
@@ -80,8 +86,12 @@ function init() {
return false
}
const found = find(renderer.root)
- if (!found) return
+ if (!found) {
+ focus = null
+ return
+ }
focus.focus()
+ focus = null
}, 1)
}
@@ -97,7 +107,7 @@ function init() {
refocus()
},
replace(input: any, onClose?: () => void) {
- if (store.stack.length === 0) {
+ if (store.stack.length === 0 && (!focus || focus.isDestroyed)) {
focus = renderer.currentFocusedRenderable
}
for (const item of store.stack) {
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index d38de8a94078..2afdc3db174a 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -369,6 +369,7 @@ export namespace Config {
tools: z.record(z.string(), z.boolean()).optional(),
disable: z.boolean().optional(),
description: z.string().optional().describe("Description of when to use the agent"),
+ short_description: z.string().optional().describe("Short description for display in agent listings"),
mode: z.enum(["subagent", "primary", "all"]).optional(),
color: z
.string()
diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts
index c640f41a7190..536decf6a7b4 100644
--- a/packages/sdk/js/src/gen/types.gen.ts
+++ b/packages/sdk/js/src/gen/types.gen.ts
@@ -965,6 +965,10 @@ export type AgentConfig = {
* Description of when to use the agent
*/
description?: string
+ /**
+ * Short description for display in agent listings
+ */
+ short_description?: string
mode?: "subagent" | "primary" | "all"
/**
* Hex color code for the agent (e.g., #FF5733)
@@ -1542,6 +1546,7 @@ export type File = {
export type Agent = {
name: string
description?: string
+ shortDescription?: string
mode: "subagent" | "primary" | "all"
builtIn: boolean
topP?: number