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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
# beans-fsb0
title: Include workspace context in quick reply prompt
status: completed
type: task
priority: normal
created_at: 2026-03-18T09:40:19Z
updated_at: 2026-03-18T09:43:07Z
parent: beans-2bbc
---

The Haiku prompt for generating quick reply suggestions should include workspace status (PR state, uncommitted changes, unpushed commits, etc.) so the suggestions are more contextually relevant. E.g. if there's an open PR with passing checks, suggest 'merge the PR' instead of generic replies.

## Summary of Changes

- Added `QuickReplyContextFunc` callback type to the agent Manager (follows existing provider pattern)
- Updated `GenerateQuickReplies` to accept optional workspace context string
- Enhanced the Haiku prompt to consider workspace status when suggesting replies
- Wired up context provider in `serve.go` that gathers: branch name, uncommitted changes, unmerged/unpushed commits, conflicts, and PR status (number, state, CI checks, review approval)

### Files modified
- `internal/agent/manager.go` — new `QuickReplyContextFunc` type and `SetQuickReplyContext` method
- `internal/agent/quickreplies.go` — updated prompt and `GenerateQuickReplies` signature
- `internal/commands/serve.go` — workspace context provider registration
11 changes: 11 additions & 0 deletions internal/agent/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ type OnFirstUserMessageFunc func(beanID string, message string)
// Receives the beanID (which is the worktree ID for workspace agents).
type OnTurnCompleteFunc func(beanID string)

// QuickReplyContextFunc returns workspace context to include in the quick reply
// generation prompt for the given beanID. Return "" to skip context injection.
type QuickReplyContextFunc func(beanID string) string

// DefaultMode controls the initial mode for new agent sessions.
type DefaultMode string

Expand Down Expand Up @@ -52,6 +56,7 @@ type Manager struct {
systemPromptProvider SystemPromptProvider
onFirstUserMessage OnFirstUserMessageFunc
onTurnComplete OnTurnCompleteFunc
quickReplyContext QuickReplyContextFunc
defaultMode DefaultMode
defaultEffort EffortLevel

Expand Down Expand Up @@ -109,6 +114,12 @@ func (m *Manager) SetOnTurnComplete(fn OnTurnCompleteFunc) {
m.onTurnComplete = fn
}

// SetQuickReplyContext registers a callback that returns workspace context
// (PR status, git state, etc.) for inclusion in quick reply generation prompts.
func (m *Manager) SetQuickReplyContext(fn QuickReplyContextFunc) {
m.quickReplyContext = fn
}

// GetSession returns a snapshot of the session for the given beanID, or nil.
// If no in-memory session exists but a persisted conversation is found, it is loaded.
func (m *Manager) GetSession(beanID string) *Session {
Expand Down
38 changes: 30 additions & 8 deletions internal/agent/quickreplies.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,40 @@ import (
"time"
)

const quickRepliesPrompt = `You are given the last message from an AI coding assistant. Suggest 3-4 short replies (under 10 words each) that the user might want to send next. Focus on the most likely actions: approving work, asking for changes, requesting more detail, etc.
const quickRepliesPrompt = `You are given the last message from an AI coding assistant, along with the current workspace status. Suggest 3-4 short replies (under 10 words each) that the user might want to send next.

Consider the workspace status when making suggestions. For example:
- If there are uncommitted changes, suggest committing
- If there are unpushed commits and no PR exists, suggest creating a PR
- If a PR exists with passing checks, suggest merging
- If there are conflicts with the base branch, suggest rebasing

Focus on the most likely next actions: approving work, committing, creating/updating PRs, asking for changes, requesting more detail, etc.

Output one reply per line, nothing else. No numbering, no bullets, no quotes.

Examples of good replies:
Yes, implement this
Commit these changes
Create a PR
Show me the code first
What about error handling?
Let's skip this for now

Assistant message:`
Let's skip this for now`

// GenerateQuickReplies runs a lightweight Claude Haiku call to suggest
// follow-up replies based on the last assistant message.
// follow-up replies based on the last assistant message and workspace context.
// workspaceContext is optional — pass "" to omit it.
// Returns a slice of suggestion strings, or nil on error.
func GenerateQuickReplies(message string) []string {
prompt := quickRepliesPrompt + "\n\n" + truncate(message, 2000)
func GenerateQuickReplies(message string, workspaceContext string) []string {
var sb strings.Builder
sb.WriteString(quickRepliesPrompt)
if workspaceContext != "" {
sb.WriteString("\n\nWorkspace status:\n")
sb.WriteString(workspaceContext)
}
sb.WriteString("\n\nAssistant message:\n\n")
sb.WriteString(truncate(message, 2000))
prompt := sb.String()

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
Expand Down Expand Up @@ -71,7 +88,12 @@ func (m *Manager) generateQuickReplies(beanID string) {
return
}

replies := GenerateQuickReplies(lastAssistant)
var wsContext string
if m.quickReplyContext != nil {
wsContext = m.quickReplyContext(beanID)
}

replies := GenerateQuickReplies(lastAssistant, wsContext)
if len(replies) == 0 {
return
}
Expand Down
56 changes: 56 additions & 0 deletions internal/commands/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/spf13/cobra"

"github.com/hmans/beans/internal/agent"
"github.com/hmans/beans/internal/gitutil"
"github.com/hmans/beans/internal/cors"
"github.com/hmans/beans/internal/graph"
"github.com/hmans/beans/internal/portalloc"
Expand Down Expand Up @@ -267,6 +268,61 @@ func runServer(port int, origins []string) error {
fmt.Printf("[beans] detected forge: %s (using %s CLI)\n", forgeProvider.Name(), forgeProvider.CLIName())
}

// Provide workspace context (PR status, git state) for quick reply generation.
agentMgr.SetQuickReplyContext(func(beanID string) string {
if beanID == graph.CentralSessionID || wtManager == nil {
return ""
}
var branch, wtPath string
if wts, err := wtManager.List(); err == nil {
for _, wt := range wts {
if wt.ID == beanID {
branch = wt.Branch
wtPath = wt.Path
break
}
}
}
if wtPath == "" {
return ""
}

var lines []string
if branch != "" {
lines = append(lines, fmt.Sprintf("Branch: %s", branch))
}
if gitutil.HasChanges(wtPath) {
lines = append(lines, "Has uncommitted changes: yes")
}
if gitutil.HasUnmergedCommits(wtPath, wtManager.BaseRef()) {
lines = append(lines, "Has commits not yet merged to base branch: yes")
}
if gitutil.HasUnpushedCommits(wtPath) {
lines = append(lines, "Has unpushed commits: yes")
}
if gitutil.HasConflicts(wtPath, wtManager.BaseRef()) {
lines = append(lines, "Has conflicts with base branch: yes")
}
if forgeProvider != nil && branch != "" {
if pr, _ := forgeProvider.FindPR(context.Background(), projectRoot, branch); pr != nil {
state := pr.State
if pr.IsDraft {
state = "draft"
}
lines = append(lines, fmt.Sprintf("Pull request: #%d (%s)", pr.Number, state))
if pr.Checks != "" {
lines = append(lines, fmt.Sprintf("CI checks: %s", pr.Checks))
}
if pr.ReviewApproved {
lines = append(lines, "Review: approved")
}
} else {
lines = append(lines, "Pull request: none")
}
}
return strings.Join(lines, "\n")
})

// Create GraphQL server with explicit transports
es := graph.NewExecutableSchema(graph.Config{
Resolvers: &graph.Resolver{
Expand Down
Loading