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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `lich sessions --json`, `lich whoami --json` and the MCP tool `list_sessions` carry the line a session's card shows under its label: `tool` and `detail` while it is busy, the tool it is running and what it acts on, and `reason` while it waits on you. A script or a desktop widget can show what each agent is doing without opening the window.

### Fixed

- Notes from the session browser, and the Pulls screen's Fix CI errors and Resolve conflicts, reach a Claude Code session you stopped with Esc. Claude Code turned the message down until someone typed at the session, and the tray said "its mod refused it"; lich now types the message at the prompt instead.

## [0.81.0] - 2026-10-28

> [!IMPORTANT]
Expand Down
12 changes: 11 additions & 1 deletion internal/relay/submit.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package relay

import (
"context"
"errors"
"fmt"

"github.com/lichdotdev/lich/api"
Expand All @@ -19,6 +20,10 @@ import (
// relayed prompt is: it carries text strangers wrote (an issue body, a
// comment), and an ESC[201~ in it would close the paste and type the rest as
// keys.
//
// A message the mod refuses is typed instead: Claude Code turns a mod's prompt
// down once the user stopped the session, until someone types at it, and the
// person at the window is that someone.
func (s *Service) Submit(ctx context.Context, opts api.SubmitOptions) (api.None, error) {
sess, ok := s.sessionOf(opts.SessionID)
if !ok {
Expand All @@ -34,5 +39,10 @@ func (s *Service) Submit(ctx context.Context, opts api.SubmitOptions) (api.None,
if !live {
return api.None{}, fmt.Errorf("%q is not running", sess.Label)
}
return api.None{}, s.deliver(opts.SessionID, sanitize(opts.Message), nil)
message := sanitize(opts.Message)
err = s.deliver(opts.SessionID, message, nil)
if errors.Is(err, errModRefused) {
err = s.typeIn(opts.SessionID, message)
}
return api.None{}, err
}
17 changes: 17 additions & 0 deletions internal/relay/submit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,20 @@ func TestSubmitRefusesWhatCannotTakeAPrompt(t *testing.T) {
t.Errorf("typed at shell = %q, want nothing", got)
}
}

// Claude Code's mod refuses a prompt once the user stopped the session, until
// someone types at it: the window's sender is that someone, so it types.
func TestSubmitTypesWhatTheModRefused(t *testing.T) {
t.Parallel()
term := newFakeTerminal("modded")
term.attachMod("modded", PromptReceipt{State: PromptAcked, Reason: "the session was stopped and nobody has sent input since"})
svc := newRelay(merging(), term, nil)

if _, err := svc.Submit(t.Context(), api.SubmitOptions{SessionID: "modded", Message: "fix the button"}); err != nil {
t.Fatalf("Submit = %v, want nil", err)
}
writes := term.writesTo("modded")
if len(writes) != 2 || writes[0] != "\x1b[200~fix the button\x1b[201~" || writes[1] != "\r" {
t.Errorf("writes = %q, want the paste and then Enter", writes)
}
}
5 changes: 4 additions & 1 deletion internal/relay/viamod.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ func (s *Service) takesDelivery(id string) (bool, error) {
return ready || s.term.ModAttached(id), nil
}

// errModRefused is a mod's ack that turned the prompt down, with its reason.
var errModRefused = errors.New("its mod refused it")

// handToMod hands a message to the session's mod, as a notification when note
// is not nil, and reports whether it is the mod's to deliver now: false with no
// error means type it instead, which is what a session with no mod and a prompt
Expand All @@ -89,7 +92,7 @@ func (s *Service) handToMod(sessionID, message string, note *Notification) (bool
return true, errors.New("the session ended before its mod took it")
case PromptAcked:
if !receipt.OK {
return true, fmt.Errorf("its mod refused it: %s", receipt.Reason)
return true, fmt.Errorf("%w: %s", errModRefused, receipt.Reason)
}
}
return true, nil
Expand Down
Loading