/ keybindings / schema.ts
schema.ts
  1  /**
  2   * Zod schema for keybindings.json configuration.
  3   * Used for validation and JSON schema generation.
  4   */
  5  
  6  import { z } from 'zod/v4'
  7  import { lazySchema } from '../utils/lazySchema.js'
  8  
  9  /**
 10   * Valid context names where keybindings can be applied.
 11   */
 12  export const KEYBINDING_CONTEXTS = [
 13    'Global',
 14    'Chat',
 15    'Autocomplete',
 16    'Confirmation',
 17    'Help',
 18    'Transcript',
 19    'HistorySearch',
 20    'Task',
 21    'ThemePicker',
 22    'Settings',
 23    'Tabs',
 24    // New contexts for keybindings migration
 25    'Attachments',
 26    'Footer',
 27    'MessageSelector',
 28    'DiffDialog',
 29    'ModelPicker',
 30    'Select',
 31    'Plugin',
 32  ] as const
 33  
 34  /**
 35   * Human-readable descriptions for each keybinding context.
 36   */
 37  export const KEYBINDING_CONTEXT_DESCRIPTIONS: Record<
 38    (typeof KEYBINDING_CONTEXTS)[number],
 39    string
 40  > = {
 41    Global: 'Active everywhere, regardless of focus',
 42    Chat: 'When the chat input is focused',
 43    Autocomplete: 'When autocomplete menu is visible',
 44    Confirmation: 'When a confirmation/permission dialog is shown',
 45    Help: 'When the help overlay is open',
 46    Transcript: 'When viewing the transcript',
 47    HistorySearch: 'When searching command history (ctrl+r)',
 48    Task: 'When a task/agent is running in the foreground',
 49    ThemePicker: 'When the theme picker is open',
 50    Settings: 'When the settings menu is open',
 51    Tabs: 'When tab navigation is active',
 52    Attachments: 'When navigating image attachments in a select dialog',
 53    Footer: 'When footer indicators are focused',
 54    MessageSelector: 'When the message selector (rewind) is open',
 55    DiffDialog: 'When the diff dialog is open',
 56    ModelPicker: 'When the model picker is open',
 57    Select: 'When a select/list component is focused',
 58    Plugin: 'When the plugin dialog is open',
 59  }
 60  
 61  /**
 62   * All valid keybinding action identifiers.
 63   */
 64  export const KEYBINDING_ACTIONS = [
 65    // App-level actions (Global context)
 66    'app:interrupt',
 67    'app:exit',
 68    'app:toggleTodos',
 69    'app:toggleTranscript',
 70    'app:toggleBrief',
 71    'app:toggleTeammatePreview',
 72    'app:toggleTerminal',
 73    'app:redraw',
 74    'app:globalSearch',
 75    'app:quickOpen',
 76    // History navigation
 77    'history:search',
 78    'history:previous',
 79    'history:next',
 80    // Chat input actions
 81    'chat:cancel',
 82    'chat:killAgents',
 83    'chat:cycleMode',
 84    'chat:modelPicker',
 85    'chat:fastMode',
 86    'chat:thinkingToggle',
 87    'chat:submit',
 88    'chat:newline',
 89    'chat:undo',
 90    'chat:externalEditor',
 91    'chat:stash',
 92    'chat:imagePaste',
 93    'chat:messageActions',
 94    // Autocomplete menu actions
 95    'autocomplete:accept',
 96    'autocomplete:dismiss',
 97    'autocomplete:previous',
 98    'autocomplete:next',
 99    // Confirmation dialog actions
100    'confirm:yes',
101    'confirm:no',
102    'confirm:previous',
103    'confirm:next',
104    'confirm:nextField',
105    'confirm:previousField',
106    'confirm:cycleMode',
107    'confirm:toggle',
108    'confirm:toggleExplanation',
109    // Tabs navigation actions
110    'tabs:next',
111    'tabs:previous',
112    // Transcript viewer actions
113    'transcript:toggleShowAll',
114    'transcript:exit',
115    // History search actions
116    'historySearch:next',
117    'historySearch:accept',
118    'historySearch:cancel',
119    'historySearch:execute',
120    // Task/agent actions
121    'task:background',
122    // Theme picker actions
123    'theme:toggleSyntaxHighlighting',
124    // Help menu actions
125    'help:dismiss',
126    // Attachment navigation (select dialog image attachments)
127    'attachments:next',
128    'attachments:previous',
129    'attachments:remove',
130    'attachments:exit',
131    // Footer indicator actions
132    'footer:up',
133    'footer:down',
134    'footer:next',
135    'footer:previous',
136    'footer:openSelected',
137    'footer:clearSelection',
138    'footer:close',
139    // Message selector (rewind) actions
140    'messageSelector:up',
141    'messageSelector:down',
142    'messageSelector:top',
143    'messageSelector:bottom',
144    'messageSelector:select',
145    // Diff dialog actions
146    'diff:dismiss',
147    'diff:previousSource',
148    'diff:nextSource',
149    'diff:back',
150    'diff:viewDetails',
151    'diff:previousFile',
152    'diff:nextFile',
153    // Model picker actions (ant-only)
154    'modelPicker:decreaseEffort',
155    'modelPicker:increaseEffort',
156    // Select component actions (distinct from confirm: to avoid collisions)
157    'select:next',
158    'select:previous',
159    'select:accept',
160    'select:cancel',
161    // Plugin dialog actions
162    'plugin:toggle',
163    'plugin:install',
164    // Permission dialog actions
165    'permission:toggleDebug',
166    // Settings config panel actions
167    'settings:search',
168    'settings:retry',
169    'settings:close',
170    // Voice actions
171    'voice:pushToTalk',
172  ] as const
173  
174  /**
175   * Schema for a single keybinding block.
176   */
177  export const KeybindingBlockSchema = lazySchema(() =>
178    z
179      .object({
180        context: z
181          .enum(KEYBINDING_CONTEXTS)
182          .describe(
183            'UI context where these bindings apply. Global bindings work everywhere.',
184          ),
185        bindings: z
186          .record(
187            z
188              .string()
189              .describe('Keystroke pattern (e.g., "ctrl+k", "shift+tab")'),
190            z
191              .union([
192                z.enum(KEYBINDING_ACTIONS),
193                z
194                  .string()
195                  .regex(/^command:[a-zA-Z0-9:\-_]+$/)
196                  .describe(
197                    'Command binding (e.g., "command:help", "command:compact"). Executes the slash command as if typed.',
198                  ),
199                z.null().describe('Set to null to unbind a default shortcut'),
200              ])
201              .describe(
202                'Action to trigger, command to invoke, or null to unbind',
203              ),
204          )
205          .describe('Map of keystroke patterns to actions'),
206      })
207      .describe('A block of keybindings for a specific context'),
208  )
209  
210  /**
211   * Schema for the entire keybindings.json file.
212   * Uses object wrapper format with optional $schema and $docs metadata.
213   */
214  export const KeybindingsSchema = lazySchema(() =>
215    z
216      .object({
217        $schema: z
218          .string()
219          .optional()
220          .describe('JSON Schema URL for editor validation'),
221        $docs: z.string().optional().describe('Documentation URL'),
222        bindings: z
223          .array(KeybindingBlockSchema())
224          .describe('Array of keybinding blocks by context'),
225      })
226      .describe(
227        'Claude Code keybindings configuration. Customize keyboard shortcuts by context.',
228      ),
229  )
230  
231  /**
232   * TypeScript types derived from the schema.
233   */
234  export type KeybindingsSchemaType = z.infer<
235    ReturnType<typeof KeybindingsSchema>
236  >