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
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,42 @@ Useful controls:
| `Enter` | send the prompt |
| `/` | open slash-command suggestions |
| `Ctrl+X` then letter | common slash commands (e.g. `m` → `/model`; `Ctrl+X` `?` for full list) |
| `Ctrl+P` / `Ctrl+N` | previous / next item in menus (arrows still work) |
| `Ctrl+P` / `Ctrl+N` | previous / next item in menus (arrows still work; not remappable) |
| `Esc` / `Ctrl+G` | dismiss a popup / cancel (Ctrl+G is emacs abort; not remappable) |
| `Shift+Tab` | cycle permission mode |
| `Ctrl+Y` | expand / collapse the plan panel |
| `Ctrl+B` | show/hide the sidebar |
| `Ctrl+C` | cancel or exit |

#### Leader keybinds (`keybindings.json`)

The leader prefix and slash-command chords live in `keybindings.json`, created
automatically next to `config.json` when missing:

| Layer | Path |
|---|---|
| User | `~/.config/zero/keybindings.json` |
| Project | `<workspace>/.zero/keybindings.json` |

Schema (slash → letter, or `""` to leave unassigned / unbind):

```json
{
"leaderKey": "ctrl+x",
"leader": {
"/model": "m",
"/provider": "p",
"/theme": "",
"/clear": ""
}
}
```

- Seeded with **every** builtin slash command; defaults match the table above.
- Project overlays user; omitted keys keep the previous layer.
- `leaderKey` must include a modifier (`ctrl` / `alt` / `cmd`).
- Global toggles (`togglePlan`, `toggleSidebar`, …) stay under `config.json` → `keybindings`.

Common slash commands:

| Command | Purpose |
Expand Down
36 changes: 36 additions & 0 deletions internal/cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,10 @@ func runInteractiveTUIWithSetup(stderr io.Writer, deps appDeps, permissionMode a
if userConfigPath != "" {
sttDownloadRoot = filepath.Join(filepath.Dir(userConfigPath), "stt")
}
// Leader keybindings.json: register slash catalog for auto-seed, ensure files
// exist next to config when missing, then resolve defaults ← user ← project.
config.SetKeybindingsPrimarySlashes(tui.PrimarySlashNames())
keybindingsFile := loadResolvedKeybindings(userConfigPath, projectConfigPath, workspaceRoot)
// Build the hooks dispatcher out of the AgentOptions literal so its trust skip
// report can be combined with the plugin activation's, and emit at most one
// notice when project hooks/plugins were dropped for an untrusted workspace.
Expand Down Expand Up @@ -864,6 +868,7 @@ func runInteractiveTUIWithSetup(stderr io.Writer, deps appDeps, permissionMode a
PermissionMode: permissionMode,
Notify: resolved.Notify,
KeyBindings: resolved.KeyBindings,
KeybindingsFile: keybindingsFile,
STT: resolved.STT,
BuildDictationTranscriber: newDictationTranscriberFactory(resolved, userConfigPath, sttServerManager),
ShutdownDictationServer: sttServerManager.Shutdown,
Expand All @@ -882,6 +887,37 @@ func runInteractiveTUIWithSetup(stderr io.Writer, deps appDeps, permissionMode a
})
}

// loadResolvedKeybindings ensures keybindings.json next to user/project config
// when missing, loads both layers, and merges onto code defaults.
func loadResolvedKeybindings(userConfigPath, projectConfigPath, workspaceRoot string) config.KeybindingsFile {
defaults := config.DefaultKeybindingsFile(tui.PrimarySlashNames())
if userConfigPath != "" {
_ = config.EnsureKeybindingsBesideConfig(userConfigPath)
}
projectKB := ""
if projectConfigPath != "" {
projectKB = config.KeybindingsPathBeside(projectConfigPath)
_ = config.EnsureKeybindingsBesideConfig(projectConfigPath)
} else if workspaceRoot != "" {
// Project config may not exist yet; still ensure under .zero if that dir
// already has a config, otherwise only seed user path above.
candidate := filepath.Join(workspaceRoot, ".zero", "config.json")
if _, err := os.Stat(candidate); err == nil {
_ = config.EnsureKeybindingsBesideConfig(candidate)
projectKB = config.KeybindingsPathBeside(candidate)
}
}
userFile, err := config.LoadKeybindingsFile(config.KeybindingsPathBeside(userConfigPath))
if err != nil {
userFile = config.KeybindingsFile{}
}
projectFile, err := config.LoadKeybindingsFile(projectKB)
if err != nil {
projectFile = config.KeybindingsFile{}
}
return config.ResolveKeybindings(defaults, userFile, projectFile)
}

func tuiSandboxSetupCommand(backend sandbox.Backend, deps appDeps) func(context.Context) tui.SandboxSetupCommandResult {
if backend.Platform != "windows" || backend.Name != sandbox.BackendWindowsRestrictedToken || !backend.Available || backend.Executable == "" {
return nil
Expand Down
Loading
Loading