Skip to content
Merged
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
25 changes: 16 additions & 9 deletions src/renderer/src/features/command-palette/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,20 @@ const commandDefs: CommandDef[] = [
export function buildCommandRegistry(ctx: CommandContext): ResolvedCommand[] {
return commandDefs
.filter(command => (command.when ? command.when(ctx) : true))
.map(command => ({
id: command.id,
title: typeof command.title === 'function' ? command.title(ctx) : command.title,
shortcut: command.shortcut,
keywords: command.keywords ?? [],
keepPaletteOpen: command.keepPaletteOpen === true,
state: command.getState ? command.getState(ctx) : null,
run: command.run,
}))
.map(command => {
const description = command.description.trim()
if (!description) {
throw new Error(`Command ${command.id} is missing a description`)
}
return {
id: command.id,
title: typeof command.title === 'function' ? command.title(ctx) : command.title,
description,
shortcut: command.shortcut,
keywords: command.keywords ?? [],
keepPaletteOpen: command.keepPaletteOpen === true,
state: command.getState ? command.getState(ctx) : null,
run: command.run,
}
})
}
2 changes: 2 additions & 0 deletions src/renderer/src/features/command-palette/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type CommandContext = {
export type CommandDef = {
id: string
title: string | ((ctx: CommandContext) => string)
description: string
shortcut?: string
keywords?: string[]
keepPaletteOpen?: boolean
Expand All @@ -78,6 +79,7 @@ export type CommandDef = {
export type ResolvedCommand = {
id: string
title: string
description: string
shortcut?: string
keywords: string[]
keepPaletteOpen: boolean
Expand Down
88 changes: 85 additions & 3 deletions src/renderer/src/features/command-palette/ui/CommandPalette.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import ReactMarkdown from 'react-markdown'

import { buildCommandRegistry } from '@renderer/features/command-palette/registry'
import type { CommandContext, ResolvedCommand } from '@renderer/features/command-palette/types'
Expand Down Expand Up @@ -412,6 +413,11 @@ export function CommandPalette({
)
}, [mode, buried, commands, sessions, promptTemplates, query])

const selectedCommand = useMemo(() => {
if (mode !== 'commands') return null
return filtered[selectedIndex] as ResolvedCommand | undefined ?? null
}, [filtered, mode, selectedIndex])

useEffect(() => {
if (open) {
setQuery('')
Expand Down Expand Up @@ -629,7 +635,7 @@ export function CommandPalette({
>
<div
className="
mt-[12vh] w-[min(560px,90vw)]
mt-[12vh] w-[min(900px,92vw)]
max-h-[60vh] flex flex-col
bg-surface border border-border
shadow-lg shadow-black/30
Expand Down Expand Up @@ -707,7 +713,16 @@ export function CommandPalette({
/>
</div>

<div ref={listRef} className="flex-1 overflow-y-auto py-1">
<div className="flex-1 min-h-0 flex overflow-hidden">
<div
ref={listRef}
className={`
min-h-0 py-1
${mode === 'commands'
? 'flex-1 min-w-0 overflow-y-auto md:basis-[70%] md:border-r md:border-border'
: 'flex-1 overflow-y-auto'}
`}
>
{(mode === 'save-prompt-template' || mode === 'edit-prompt-template') && (
<div className="px-3 py-3 space-y-3">
<textarea
Expand Down Expand Up @@ -964,8 +979,75 @@ export function CommandPalette({
</div>
))
))}
</div>

{mode === 'commands' && (
<CommandDescriptionPanel command={selectedCommand} />
)}
</div>
</div>
</div>
)
}

const COMMAND_DESCRIPTION_COMPONENTS: import('react-markdown').Options['components'] = {
p: ({ children }) => (
<p className="mb-2 text-[11px] leading-[1.55] text-ink-dim last:mb-0">
{children}
</p>
),
strong: ({ children }) => (
<strong className="font-semibold text-ink">{children}</strong>
),
}

const CommandDescriptionPanel = memo(function CommandDescriptionPanel({
command,
}: {
command: ResolvedCommand | null
}) {
if (!command) {
return (
<aside
role="region"
aria-label="Command details"
className="hidden basis-[30%] min-w-[220px] bg-canvas px-4 py-4 text-[12px] text-muted md:block"
>
Select a command to see what it does.
</aside>
)
}

return (
<aside
role="region"
aria-label="Command details"
className="hidden basis-[30%] min-w-[220px] overflow-y-auto bg-canvas px-4 py-4 md:block"
>
<div className="mb-3 border-b border-border pb-3">
<div className="text-[13px] text-ink">{command.title}</div>
<div className="mt-1 flex items-center gap-2 text-[10px] text-muted">
{command.shortcut && <span>{command.shortcut}</span>}
{command.state && (
<span
className={
command.state.tone === 'danger'
? 'border border-red-600/40 bg-red-500/10 px-1.5 py-0.5 uppercase tracking-wider text-red-300'
: command.state.tone === 'accent'
? 'border border-accent/30 bg-accent/10 px-1.5 py-0.5 uppercase tracking-wider text-accent'
: 'border border-border bg-surface-hi px-1.5 py-0.5 uppercase tracking-wider text-muted'
}
>
{command.state.label}
</span>
)}
</div>
</div>
<div>
<ReactMarkdown components={COMMAND_DESCRIPTION_COMPONENTS}>
{command.description}
</ReactMarkdown>
</div>
</aside>
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const copyAssistantCommands: CommandDef[] = [
{
id: 'copy-assistant-message',
title: 'Copy Assistant Message…',
description: '**What it does:** Opens a picker to copy a specific **assistant message**.\n\n**Use when:** You need an older response, not just the latest one.\n\n**Notes:** Use arrows, **Enter**, and **Esc** after opening.',
keywords: ['copy', 'assistant', 'message', 'response', 'pick'],
when: ({ workspace }) => commandTargetSessionId(workspace) !== null,
run: ({ workspace }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const promptTemplateCommands: CommandDef[] = [
{
id: 'prompt-template',
title: 'Prompt Template…',
description: '**What it does:** Inserts a saved **prompt template** into the focused composer.\n\n**Use when:** You want reusable prompt text without retyping it.\n\n**Notes:** Agent panes only.',
keywords: ['prompt', 'template', 'snippet', 'insert', 'draft'],
keepPaletteOpen: true,
when: ({ workspace }) => focusedAgentSessionId(workspace) !== null,
Expand All @@ -21,6 +22,7 @@ export const promptTemplateCommands: CommandDef[] = [
{
id: 'save-composer-as-prompt-template',
title: 'Save Composer as Prompt Template…',
description: '**What it does:** Saves current composer text as a **custom prompt template**.\n\n**Use when:** You wrote a prompt you expect to reuse.\n\n**Notes:** Only appears when the composer has text.',
keywords: ['prompt', 'template', 'save', 'composer', 'custom', 'snippet'],
keepPaletteOpen: true,
when: ({ workspace }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const readerCommands: CommandDef[] = [
{
id: 'toggle-reader-mode',
title: 'Reader Mode',
description: '**What it does:** Toggles a cleaner **reading view** for the current agent.\n\n**Use when:** You want to read long agent output comfortably.\n\n**Notes:** Uses the focused command target.',
keywords: ['reader', 'read', 'focus', 'plan', 'response', 'zen'],
getState: ({ workspace }) => ({
label: workspace.readerMode ? 'On' : 'Off',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const dangerousCommands: CommandDef[] = [
{
id: 'dangerous-agents',
title: 'Dangerous Agents',
description: '**What it does:** Toggles **dangerous agent mode** for future agents.\n\n**Use when:** You explicitly want agents to run with fewer safety restrictions.\n\n**Notes:** Affects new agent sessions, not existing ones.',
getState: ({ flags }) => ({
label: flags.dangerousAgentsEnabled ? 'On' : 'Off',
tone: flags.dangerousAgentsEnabled ? 'danger' : 'neutral',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ export const settingsCommands: CommandDef[] = [
{
id: 'open-settings',
title: 'Open Settings',
description: '**What it does:** Opens **Settings**.\n\n**Use when:** You want to change app preferences.\n\n**Notes:** Includes appearance, workspace, dictation, experimental, and safety settings.',
run: ({ ui }) => ui.openSettings(),
},
{
id: 'toggle-custom-rendering',
title: 'Custom Rendering',
description: '**What it does:** Toggles the custom **feed renderer**.\n\n**Use when:** You want to compare custom rendering with the fallback path.\n\n**Notes:** Rendering preference only.',
getState: ({ flags }) => ({
label: flags.customRenderingEnabled ? 'On' : 'Off',
tone: flags.customRenderingEnabled ? 'accent' : 'neutral',
Expand All @@ -24,6 +26,7 @@ export const settingsCommands: CommandDef[] = [
// bundles, not per-frame debug overhead.
id: 'toggle-aggressive-debug-persistence',
title: 'Persistent Aggressive Debug Logs',
description: '**What it does:** Periodically saves **debug bundles** for active agents.\n\n**Use when:** You are chasing crashes or disappearing state.\n\n**Notes:** Can create many or large debug files.',
keywords: [
'debug',
'logs',
Expand All @@ -45,6 +48,7 @@ export const settingsCommands: CommandDef[] = [
{
id: 'toggle-worktrees-bar',
title: 'Worktrees',
description: '**What it does:** Shows or hides the **Worktrees** panel.\n\n**Use when:** You want branch and worktree activity for the focused project.\n\n**Notes:** Useful for multi-agent git cleanup.',
keywords: ['worktree', 'worktrees', 'branch', 'git', 'activity', 'agents', 'cleanup'],
getState: ({ flags }) => ({
label: flags.worktreesBarOpen ? 'Open' : 'Closed',
Expand All @@ -55,6 +59,7 @@ export const settingsCommands: CommandDef[] = [
{
id: 'toggle-worktree-badges',
title: 'Worktree Badges',
description: '**What it does:** Toggles **worktree badges** on agent rows.\n\n**Use when:** You want branch context visible in panes and **Dispatch**.\n\n**Notes:** Visual-only setting.',
keywords: ['branch', 'git', 'worktree', 'badge', 'agent'],
getState: ({ flags }) => ({
label: flags.worktreeBadgesEnabled ? 'On' : 'Off',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const spotlightCommands: CommandDef[] = [
{
id: 'toggle-spotlight',
title: 'Spotlight',
description: '**What it does:** Toggles a focused **single-pane view**.\n\n**Use when:** You want one session large without changing the grid layout.\n\n**Notes:** Press **Esc** to exit.',
getState: ({ workspace }) => ({
label: workspace.spotlight ? 'On' : 'Off',
tone: workspace.spotlight ? 'accent' : 'neutral',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const tileTabsCommands: CommandDef[] = [
{
id: 'tiled-tabs',
title: 'Tiled Tabs',
description: '**What it does:** Opens a modal to choose tabs for a **tiled tab view**.\n\n**Use when:** You want multiple tabs visible at once.\n\n**Notes:** If tiled tabs are already open, this command closes the tiled view.',
getState: ({ workspace }) => ({
label: workspace.tileTabs ? 'On' : 'Off',
tone: workspace.tileTabs ? 'accent' : 'neutral',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const layoutCommands: CommandDef[] = [
{
id: 'dispatch-mode',
title: 'Dispatch Mode',
description: '**What it does:** Toggles the **Dispatch** command-center layout.\n\n**Use when:** You want to scan and command agents from a compact list.\n\n**Notes:** Shows the selected agent, the agent list, and an optional project terminal.',
keywords: ['agent list', 'focused agent', 'command center'],
getState: ({ flags }) => ({
label: flags.dispatchModeEnabled
Expand All @@ -22,6 +23,7 @@ export const layoutCommands: CommandDef[] = [
{
id: 'global-dispatch',
title: 'Global Dispatch',
description: '**What it does:** Switches **Dispatch** between project scope and all-tabs scope.\n\n**Use when:** You want one command center for agents across every tab.\n\n**Notes:** Only appears while **Dispatch Mode** is enabled.',
keywords: ['dispatch all tabs', 'agent list'],
when: ({ flags }) => flags.dispatchModeEnabled,
getState: ({ flags }) => ({
Expand All @@ -35,13 +37,15 @@ export const layoutCommands: CommandDef[] = [
{
id: 'exit-dispatch-mode',
title: 'Exit Dispatch Mode',
description: '**What it does:** Leaves **Dispatch** and returns to the normal grid layout.\n\n**Use when:** You are done using the command-center view.\n\n**Notes:** Detached **Dispatch** agents stay parked until you attach them.',
keywords: ['grid mode', 'normal layout'],
when: ({ flags }) => flags.dispatchModeEnabled,
run: ({ ui }) => ui.exitDispatchMode(),
},
{
id: 'toggle-dispatch-terminal',
title: 'Dispatch Terminal',
description: '**What it does:** Shows or hides the project terminal inside **Dispatch**.\n\n**Use when:** You want shell access beside the selected agent.\n\n**Notes:** The terminal is tied to the active project tab.',
keywords: ['project terminal', 'right terminal'],
when: ({ flags }) => flags.dispatchModeEnabled,
getState: ({ flags }) => ({
Expand All @@ -55,21 +59,25 @@ export const layoutCommands: CommandDef[] = [
{
id: 'normalize-layout',
title: 'Normalize Layout',
description: '**What it does:** Rebalances pane sizes in the current layout.\n\n**Use when:** Panes feel uneven but the layout shape is still useful.\n\n**Notes:** Keeps the same split structure.',
run: ({ workspace }) => workspace.normalizeLayout(),
},
{
id: 'hard-normalize-layout',
title: 'Hard Normalize Layout',
description: '**What it does:** Rebuilds pane sizing into a cleaner even layout.\n\n**Use when:** The layout is messy and needs a stronger reset.\n\n**Notes:** More aggressive than **Normalize Layout**.',
run: ({ workspace }) => workspace.hardNormalizeLayout(),
},
{
id: 'rotate-layout',
title: 'Rotate Layout',
description: '**What it does:** Rotates split directions in the current layout.\n\n**Use when:** The same panes would work better in a different orientation.\n\n**Notes:** Keeps the sessions, changes the arrangement.',
run: ({ workspace }) => workspace.rotateLayout(),
},
{
id: 'toggle-status-mode',
title: 'Status Mode',
description: '**What it does:** Toggles status coloring for active agents.\n\n**Use when:** You want running or working agents to stand out.\n\n**Notes:** This is a visual setting only.',
getState: ({ flags }) => ({
label: flags.statusModeEnabled ? 'On' : 'Off',
tone: flags.statusModeEnabled ? 'accent' : 'neutral',
Expand All @@ -79,6 +87,7 @@ export const layoutCommands: CommandDef[] = [
{
id: 'toggle-performance-panel',
title: 'Performance Stats',
description: '**What it does:** Shows or hides the performance stats panel.\n\n**Use when:** You want render, pane, or runtime performance details.\n\n**Notes:** Mostly useful while debugging the app.',
keywords: ['performance', 'stats', 'cpu', 'memory', 'panes'],
getState: ({ flags }) => ({
label: flags.performancePanelOpen ? 'On' : 'Off',
Expand Down
Loading