jesseduffield / jesseduffield/lazygit

Custom commands: add `loadingText` to prompts so `runCommand` in `initialValue` shows progress

Open
#5,627 0 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
Go
Stars
82.4k
Forks
3k
Avg merge
2d 18h
Merged PRs (30d)
19

Description

### Background

`customCommands` supports a `runCommand` template helper that can be used inside a prompt's `initialValue`, e.g. to pre-fill a commit message generated by an external script:

```yaml
customCommands:
- key: ''
context: 'files'
prompts:
- type: 'input'
title: 'Commit message'
key: 'Msg'
initialValue: '{{ runCommand "ai-commit.sh" }}'
command: 'git commit -m {{.Form.Msg | quote}}'
loadingText: 'Committing...'
```

This pattern is increasingly useful for AI-assisted commits, ticket-number lookups, branch-name suggestions, etc.

### Problem

When the script in `runCommand` takes a few seconds (typical for an LLM API call: 2–5s), lazygit appears to freeze:

- No spinner is shown.
- The status bar doesn't update.
- The user can't tell whether lazygit is hung, the script crashed, or it's still working.

The existing `loadingText` field on the custom command only wraps the *final* `command` execution (via `WithWaitingStatus` in `finalHandler`). It is **not** applied while a prompt's template (including `initialValue` / `suggestions.command` / `menuFromCommand.command`) is being resolved.

Relevant code path: [`pkg/gui/services/custom_commands/handler_creator.go`](https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/services/custom_commands/handler_creator.go) — each `case` in the prompt switch calls `self.resolver.resolvePrompt(...)` synchronously, and `menuPromptFromCommand` additionally calls `RunWithOutput` synchronously, neither wrapped in `WithWaitingStatus`.

### Steps to reproduce

1. Add the custom command above to `config.yml`.
2. Replace `ai-commit.sh` with anything that takes ~3s, e.g. `sh -c 'sleep 3; echo "feat: test"'`.
3. Stage a file and press `` in the Files panel.
4. Observe: lazygit appears frozen for ~3s with no indication that work is happening, before the input popup eventually appears with the pre-filled value.

### Proposed solution

Add a `loadingText` field to `CustomCommandPrompt` and, when set, wrap the prompt's resolution (and for `menuFromCommand`, the menu command execution) in `WithWaitingStatus`.

```yaml
prompts:
- type: 'input'
title: 'Commit message'
key: 'Msg'
loadingText: 'Generating commit message...' # ← new
initialValue: '{{ runCommand "ai-commit.sh" }}'
```

Sketch of the change in `handler_creator.go`:

```go
// helper
func (self *HandlerCreator) resolveWithLoading(
prompt *config.CustomCommandPrompt,
resolveTemplate func(string) (string, error),
) (*config.CustomCommandPrompt, error) {
var resolved *config.CustomCommandPrompt
var resolveErr error
doResolve := func() { resolved, resolveErr = self.resolver.resolvePrompt(prompt, resolveTemplate) }

if prompt.LoadingText != "" {
if err := self.c.WithWaitingStatus(prompt.LoadingText, func(gocui.Task) error {
doResolve()
return nil
}); err != nil {
return nil, err
}
} else {
doResolve()
}
return resolved, resolveErr
}
```

Then in the existing switch:

```go
case "input":
f = func() error {
resolvedPrompt, err := self.resolveWithLoading(&prompt, resolveTemplate)
if err != nil {
return err
}
return self.inputPrompt(resolvedPrompt, wrappedF)
}
// (same pattern for menu / confirm / menuFromCommand)
```

For `menuFromCommand`, the synchronous `RunWithOutput` inside `menuPromptFromCommand` should also be moved inside `WithWaitingStatus`, while keeping the actual `self.c.Menu(...)` call outside so it runs on the UI thread.

### Alternatives considered

1. **Split into two custom commands** — first one runs the script with `loadingText` and writes to a temp file; second one reads the file via `runCommand` in `initialValue`. Works today, but costs an extra keystroke and a temp file.
2. **Tell users to make their scripts faster** — works for local models, doesn't help when a remote API is genuinely slow.
3. **Run the script asynchronously and update the prompt later** — much larger change; the proposed fix is the minimal one that gives users feedback without re-architecting prompt resolution.

### Scope / size of change

- ~30 lines in `handler_creator.go`
- 1 new field in `CustomCommandPrompt` (`pkg/config/...`)
- A short addition to `docs/Custom_Command_Keybindings.md`
- A unit test or two

### Questions for maintainers before I open a PR

1. Are you open to this addition in principle? (I want to avoid wasted work.)
2. Any preference on field name — `loadingText` (matches the existing top-level field) vs something else?
3. For `menuFromCommand`, do you prefer the loading wrap to cover both resolution and command execution as a single phase, or only the command execution?
4. Should the field also be honored when no `LoadingText` is set, defaulting to a generic "Running..." (mirroring `finalHandler`'s fallback to `Tr.RunningCustomCommandStatus`)?

Happy to put up a PR once direction is confirmed. Thanks for lazygit — it's lovely.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.