mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
tui: slash commands work while a turn is in flight
previously typing any slash command during a turn was rejected with "cancel the current turn (esc) before running a slash command". annoying and unnecessary for read-only commands, and the destructive ones can cancel the turn for you. read-only commands run immediately, parallel to the streaming turn: /help, /jump, /sessions, /lock, /unlock, /exit. destructive commands trigger cancelAndWaitForIdle first (cancels the turn ctx, polls i.busy at 10ms intervals, gives up after 2s as a safety cap so a wedged http stream cannot freeze the ui forever): /clear, /compact, /login, /logout, /model. once the turn goroutine has wound down they run on the now-quiet agent. slashCancelsTurn(head) in slash_suggest.go is the single source of truth for which commands need the wait. readme updated to match the new behaviour.
This commit is contained in:
parent
64704875d2
commit
d653cc179d
3 changed files with 59 additions and 12 deletions
|
|
@ -223,7 +223,7 @@ frames containing images are full-repainted (no differential diff) to prevent st
|
|||
|
||||
you can keep typing while the agent is working. pressing enter during a turn queues the message instead of interrupting: it shows up above the status bar as `▸ sliding in: <text>` and is delivered as the next user turn the moment the current one finishes. queue as many as you want; they run in order. esc / ctrl+c cancels the active turn and drops the queue so a runaway turn doesn't flood you with stale follow-ups.
|
||||
|
||||
slash commands still require an idle state — typing `/something` during a turn prints `cancel the current turn (esc) before running a slash command`.
|
||||
slash commands also work while the agent is busy. read-only ones (`/help`, `/jump`, `/sessions`, `/lock`, `/unlock`, `/exit`) take effect immediately. destructive ones (`/clear`, `/compact`, `/login`, `/logout`, `/model`) cancel the active turn first and then run.
|
||||
|
||||
## keys (interactive mode)
|
||||
|
||||
|
|
|
|||
|
|
@ -739,17 +739,19 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) {
|
|||
i.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
// Slash commands need a quiet state (they may swap models,
|
||||
// compact the transcript, open dialogs, etc). Refuse while
|
||||
// a turn is in flight — esc / ctrl+c cancels first.
|
||||
i.mu.Lock()
|
||||
busy := i.busy
|
||||
i.mu.Unlock()
|
||||
if busy {
|
||||
i.mu.Lock()
|
||||
i.statusErr = "cancel the current turn (esc) before running a slash command"
|
||||
i.mu.Unlock()
|
||||
return false
|
||||
// Slash commands run regardless of busy state. Commands that
|
||||
// would mutate the transcript or replace the agent (/clear,
|
||||
// /compact, /logout, /login, /model) cancel the active turn
|
||||
// first and wait for the goroutine to wind down so they don't
|
||||
// race with a streaming response. Safe commands (/help,
|
||||
// /jump, /sessions, /lock, /unlock, /exit) run immediately
|
||||
// without disturbing the active turn.
|
||||
head := text
|
||||
if idx := strings.IndexAny(text, " \t"); idx >= 0 {
|
||||
head = text[:idx]
|
||||
}
|
||||
if slashCancelsTurn(head) {
|
||||
i.cancelAndWaitForIdle()
|
||||
}
|
||||
return i.runSlash(ctx, text)
|
||||
}
|
||||
|
|
@ -943,6 +945,38 @@ func (i *Interactive) startOAuthFlow(provider string) {
|
|||
// applyModelSelection switches the active model (and provider, if the
|
||||
// new model belongs to a different one). It rebuilds the underlying
|
||||
// client when needed so the provider wire-protocol matches.
|
||||
// cancelAndWaitForIdle cancels the active turn (if any) and blocks
|
||||
// briefly until the turn goroutine has updated i.busy = false. Used
|
||||
// before destructive slash commands so transcript-mutating work
|
||||
// (/clear, /compact, /logout, /login completion, cross-provider
|
||||
// /model swap) doesn't race with the still-running stream.
|
||||
//
|
||||
// The wait is bounded; if the turn doesn't release within the timeout
|
||||
// we proceed anyway. Worst case is a brief overlap that the agent's
|
||||
// own mutex protects against.
|
||||
func (i *Interactive) cancelAndWaitForIdle() {
|
||||
i.mu.Lock()
|
||||
busy := i.busy
|
||||
cancel := i.cancelTurn
|
||||
i.mu.Unlock()
|
||||
if !busy {
|
||||
return
|
||||
}
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
i.mu.Lock()
|
||||
done := !i.busy
|
||||
i.mu.Unlock()
|
||||
if done {
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// openJumpDialog builds a /jump picker from the current transcript.
|
||||
// If the user typed "/jump foo" with a filter and it matches exactly
|
||||
// one turn, jump there directly without showing the dialog.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,19 @@ type slashCommand struct {
|
|||
Desc string
|
||||
}
|
||||
|
||||
// slashCancelsTurn reports whether the named slash command, when run
|
||||
// while a turn is in flight, requires the active turn to be cancelled
|
||||
// first. The destructive commands (those that mutate the transcript
|
||||
// or rebuild the agent) need a quiet state; the rest run alongside
|
||||
// the streaming response without trouble.
|
||||
func slashCancelsTurn(head string) bool {
|
||||
switch head {
|
||||
case "/clear", "/compact", "/logout", "/login", "/model":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// slashCatalog lists every slash command the interactive mode handles.
|
||||
// Keep in sync with runSlash().
|
||||
var slashCatalog = []slashCommand{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue