Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
8c3bcc9
feat(question): add question tool with structured UI
taciturnaxolotl Jun 4, 2026
56f7e41
feat(question): add client server integration
taciturnaxolotl Jun 4, 2026
6b0f52e
feat(question): add mouse support
taciturnaxolotl Jun 5, 2026
48baac4
feat(question): add paste support in text areas
taciturnaxolotl Jun 9, 2026
1005606
refactor: make the coordinator use a struct
taciturnaxolotl Jun 12, 2026
2edbf29
bug(client/server): fix non interactive init
taciturnaxolotl Jun 12, 2026
ec6a2a6
fix(question): fix scrollbar disappearing in single-select
taciturnaxolotl Jun 12, 2026
3da1bed
feat: add plan mode
vadiminshakov May 6, 2026
d98f86a
fix(ui): show both yolo and plan markers when both modes are active
vadiminshakov May 9, 2026
0d1c33e
fix(plan): add risks consideration
vadiminshakov May 20, 2026
df293bf
fix(ui): show plan in status bar only
vadiminshakov May 20, 2026
516890a
fix(plan): added instructs
vadiminshakov May 20, 2026
3a591b4
fix: check status exists
vadiminshakov May 20, 2026
ccae613
fix(plan): refine workflow and style guidelines for clarity and preci…
vadiminshakov May 22, 2026
6b03474
fix(plan): mutate model synchronously in toggleInputMode to prevent c…
vadiminshakov May 22, 2026
2c7d4f3
fix(plan): enhance plan mode description
vadiminshakov May 24, 2026
b957f1f
fix(plan): clarify wording for search tools in workflow instructions
vadiminshakov May 24, 2026
657346a
fix(plan): update setEditorPrompt to include mode parameter
vadiminshakov May 25, 2026
efc21a0
chore: rebase maintenance
vadiminshakov May 30, 2026
45807ed
feat: add plan mode
vadiminshakov May 6, 2026
73db244
fix(ui): show both yolo and plan markers when both modes are active
vadiminshakov May 9, 2026
8ea287f
fix(plan): add risks consideration
vadiminshakov May 20, 2026
6f4765c
fix(ui): show plan in status bar only
vadiminshakov May 20, 2026
a34218d
fix(plan): added instructs
vadiminshakov May 20, 2026
4fbc823
fix: check status exists
vadiminshakov May 20, 2026
a183889
fix(plan): refine workflow and style guidelines for clarity and preci…
vadiminshakov May 22, 2026
56f06fa
fix(plan): mutate model synchronously in toggleInputMode to prevent c…
vadiminshakov May 22, 2026
f9f014a
fix(plan): enhance plan mode description
vadiminshakov May 24, 2026
a718d08
fix(plan): clarify wording for search tools in workflow instructions
vadiminshakov May 24, 2026
4869a07
feat(plan): implement plan handoff dialog and add question tool usage
vadiminshakov Jun 9, 2026
2685dbe
fix(plan): refine question tool usage and confirmation process in pla…
vadiminshakov Jun 9, 2026
e389106
feat(plan): implement inline plan handoff prompt and update related l…
vadiminshakov Jun 9, 2026
6843896
chore(plan): fix
vadiminshakov Jun 9, 2026
b994e33
feat(plan): add plan-ready marker detection and styling for final pla…
vadiminshakov Jun 9, 2026
5a9e408
feat(plan): add "Critical Files" section to final plan response
vadiminshakov Jun 9, 2026
c7bb4f6
feat(plan): implement plan-ready marker handling and UI updates
vadiminshakov Jun 9, 2026
5df91fe
feat(styles): enhance PlanMarkdown styling for headings in quickStyle
vadiminshakov Jun 9, 2026
5520cf8
feat(styles): update PlanMarkdown to replace raw markdown prefixes wi…
vadiminshakov Jun 9, 2026
ba8d66e
chore: lint
vadiminshakov Jun 14, 2026
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
19 changes: 6 additions & 13 deletions internal/agent/agenttest/coordinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,10 @@ func NewCoordinator(
coderCfg.AllowedTools = nil
cfg.Config().Agents[config.AgentCoder] = coderCfg

return agent.NewCoordinator(
ctx,
cfg,
sessions,
messages,
permission.NewPermissionService(workingDir, true, nil),
nil,
nil,
nil,
nil,
nil,
nil,
)
return agent.NewCoordinator(ctx, agent.CoordinatorOptions{
Config: cfg,
Sessions: sessions,
Messages: messages,
Permissions: permission.NewPermissionService(workingDir, true, nil),
})
}
119 changes: 82 additions & 37 deletions internal/agent/coordinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/charmbracelet/crush/internal/oauth/copilot"
"github.com/charmbracelet/crush/internal/permission"
"github.com/charmbracelet/crush/internal/pubsub"
"github.com/charmbracelet/crush/internal/question"
"github.com/charmbracelet/crush/internal/session"
"github.com/charmbracelet/crush/internal/skills"
"golang.org/x/sync/errgroup"
Expand All @@ -52,6 +53,8 @@ import (
// Coordinator errors.
var (
errCoderAgentNotConfigured = errors.New("coder agent not configured")
errPlanAgentNotConfigured = errors.New("plan agent not configured")
errMainAgentNotFound = errors.New("main agent not found")
errModelProviderNotConfigured = errors.New("model provider not configured")
errLargeModelNotSelected = errors.New("large model not selected")
errSmallModelNotSelected = errors.New("small model not selected")
Expand All @@ -78,8 +81,7 @@ var opencodeMessagesModels = map[string]bool{
}

type Coordinator interface {
// INFO: (kujtim) this is not used yet we will use this when we have multiple agents
// SetMainAgent(string)
SetMainAgent(agentName string) error
Run(ctx context.Context, sessionID, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error)
// RunAccepted runs a call that was already accepted via
// BeginAccepted on the fire-and-forget dispatch path. The handle is
Expand Down Expand Up @@ -107,14 +109,17 @@ type coordinator struct {
sessions session.Service
messages message.Service
permissions permission.Service
questions question.Service
history history.Service
filetracker filetracker.Service
lspManager *lsp.Manager
notify pubsub.Publisher[notify.Notification]
runComplete pubsub.Publisher[notify.RunComplete]
interactive bool

currentAgent SessionAgent
agents map[string]SessionAgent
currentAgent SessionAgent
currentAgentName string
agents map[string]SessionAgent

// Skills discovery results (session-start snapshot).
allSkills []*skills.Skill // Pre-filter: all discovered after dedup.
Expand All @@ -124,68 +129,103 @@ type coordinator struct {
readyWg errgroup.Group
}

func NewCoordinator(
ctx context.Context,
cfg *config.ConfigStore,
sessions session.Service,
messages message.Service,
permissions permission.Service,
history history.Service,
filetracker filetracker.Service,
lspManager *lsp.Manager,
notify pubsub.Publisher[notify.Notification],
runComplete pubsub.Publisher[notify.RunComplete],
skillsMgr *skills.Manager,
) (Coordinator, error) {
// CoordinatorOptions holds the dependencies for NewCoordinator. Using a
// struct keeps the constructor self-documenting and avoids a long
// positional parameter list.
type CoordinatorOptions struct {
Config *config.ConfigStore
Sessions session.Service
Messages message.Service
Permissions permission.Service
Questions question.Service
History history.Service
FileTracker filetracker.Service
LSPManager *lsp.Manager
Notify pubsub.Publisher[notify.Notification]
RunComplete pubsub.Publisher[notify.RunComplete]
Skills *skills.Manager
Interactive bool
}

func NewCoordinator(ctx context.Context, opts CoordinatorOptions) (Coordinator, error) {
// Skills are pre-discovered by the caller (see app.New /
// backend.CreateWorkspace) and passed in via the manager. If no
// manager was provided (legacy callers), fall back to an in-line
// discovery so the coordinator still works.
var allSkills, activeSkills []*skills.Skill
if skillsMgr != nil {
allSkills = skillsMgr.AllSkills()
activeSkills = skillsMgr.ActiveSkills()
if opts.Skills != nil {
allSkills = opts.Skills.AllSkills()
activeSkills = opts.Skills.ActiveSkills()
} else {
allSkills, activeSkills = discoverSkills(cfg)
allSkills, activeSkills = discoverSkills(opts.Config)
}
skillTracker := skills.NewTracker(activeSkills)

c := &coordinator{
cfg: cfg,
sessions: sessions,
messages: messages,
permissions: permissions,
history: history,
filetracker: filetracker,
lspManager: lspManager,
notify: notify,
runComplete: runComplete,
cfg: opts.Config,
sessions: opts.Sessions,
messages: opts.Messages,
permissions: opts.Permissions,
questions: opts.Questions,
history: opts.History,
filetracker: opts.FileTracker,
lspManager: opts.LSPManager,
notify: opts.Notify,
runComplete: opts.RunComplete,
agents: make(map[string]SessionAgent),
allSkills: allSkills,
activeSkills: activeSkills,
skillTracker: skillTracker,
interactive: opts.Interactive,
}

agentCfg, ok := cfg.Config().Agents[config.AgentCoder]
agentCfg, ok := opts.Config.Config().Agents[config.AgentCoder]
if !ok {
return nil, errCoderAgentNotConfigured
}

// TODO: make this dynamic when we support multiple agents
prompt, err := coderPrompt(prompt.WithWorkingDir(c.cfg.WorkingDir()))
coderPrompt, err := coderPrompt(prompt.WithWorkingDir(c.cfg.WorkingDir()))
if err != nil {
return nil, err
}

agent, err := c.buildAgent(ctx, prompt, agentCfg, false)
agent, err := c.buildAgent(ctx, coderPrompt, agentCfg, false)
if err != nil {
return nil, err
}
c.currentAgent = agent
c.agents[config.AgentCoder] = agent

planCfg, ok := c.cfg.Config().Agents[config.AgentPlan]
if !ok {
return nil, errPlanAgentNotConfigured
}

planSystemPrompt, err := planPrompt(prompt.WithWorkingDir(c.cfg.WorkingDir()))
if err != nil {
return nil, err
}

planAgent, err := c.buildAgent(ctx, planSystemPrompt, planCfg, false)
if err != nil {
return nil, err
}
c.agents[config.AgentPlan] = planAgent

c.currentAgent = agent
c.currentAgentName = config.AgentCoder
return c, nil
}

func (c *coordinator) SetMainAgent(agentName string) error {
agent, ok := c.agents[agentName]
if !ok {
return fmt.Errorf("%w: %s", errMainAgentNotFound, agentName)
}
c.currentAgent = agent
c.currentAgentName = agentName
return nil
}

// Run implements Coordinator.
func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string, attachments ...message.Attachment) (*fantasy.AgentResult, error) {
return c.run(ctx, nil, sessionID, prompt, attachments...)
Expand Down Expand Up @@ -623,6 +663,11 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent, isSubA
tools.NewWriteTool(c.lspManager, c.permissions, c.history, c.filetracker, c.cfg.WorkingDir()),
)

// Question tool is interactive-only and not available to sub-agents.
if !isSubAgent && c.interactive {
allTools = append(allTools, tools.NewQuestionTool(c.questions))
}

// Add LSP tools if user has configured LSPs or auto_lsp is enabled (nil or true).
if len(c.cfg.Config().LSP) > 0 || c.cfg.Config().Options.AutoLSP == nil || *c.cfg.Config().Options.AutoLSP {
allTools = append(allTools, tools.NewDiagnosticsTool(c.lspManager), tools.NewReferencesTool(c.lspManager), tools.NewLSPRestartTool(c.lspManager))
Expand Down Expand Up @@ -1086,9 +1131,9 @@ func (c *coordinator) UpdateModels(ctx context.Context) error {
}
c.currentAgent.SetModels(large, small)

agentCfg, ok := c.cfg.Config().Agents[config.AgentCoder]
agentCfg, ok := c.cfg.Config().Agents[c.currentAgentName]
if !ok {
return errCoderAgentNotConfigured
return fmt.Errorf("%w: %s", errMainAgentNotFound, c.currentAgentName)
}

tools, err := c.buildTools(ctx, agentCfg, false)
Expand Down
32 changes: 32 additions & 0 deletions internal/agent/coordinator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,3 +426,35 @@ func TestGetProviderOptionsReasoningEffort(t *testing.T) {
})
}
}

func TestCoordinatorSetMainAgent(t *testing.T) {
t.Run("switches current agent", func(t *testing.T) {
coder := &mockSessionAgent{}
plan := &mockSessionAgent{}
coord := &coordinator{
currentAgent: coder,
currentAgentName: config.AgentCoder,
agents: map[string]SessionAgent{
config.AgentCoder: coder,
config.AgentPlan: plan,
},
}

err := coord.SetMainAgent(config.AgentPlan)
require.NoError(t, err)
assert.Equal(t, config.AgentPlan, coord.currentAgentName)
assert.Same(t, plan, coord.currentAgent)
})

t.Run("returns error for unknown agent", func(t *testing.T) {
coord := &coordinator{
agents: map[string]SessionAgent{
config.AgentCoder: &mockSessionAgent{},
},
}

err := coord.SetMainAgent("unknown")
require.Error(t, err)
assert.ErrorIs(t, err, errMainAgentNotFound)
})
}
11 changes: 11 additions & 0 deletions internal/agent/prompts.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ var coderPromptTmpl []byte
//go:embed templates/task.md.tpl
var taskPromptTmpl []byte

//go:embed templates/plan.md.tpl
var planPromptTmpl []byte

//go:embed templates/initialize.md.tpl
var initializePromptTmpl []byte

Expand All @@ -33,6 +36,14 @@ func taskPrompt(opts ...prompt.Option) (*prompt.Prompt, error) {
return systemPrompt, nil
}

func planPrompt(opts ...prompt.Option) (*prompt.Prompt, error) {
systemPrompt, err := prompt.NewPrompt("plan", string(planPromptTmpl), opts...)
if err != nil {
return nil, err
}
return systemPrompt, nil
}

func InitializePrompt(cfg *config.ConfigStore) (string, error) {
systemPrompt, err := prompt.NewPrompt("initialize", string(initializePromptTmpl))
if err != nil {
Expand Down
54 changes: 54 additions & 0 deletions internal/agent/templates/plan.md.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
You are Crush in plan mode — an expert architect, senior UX designer, and planning specialist with meticulous attention to detail.

Your job is to analyze the codebase and user intent, then produce a concrete, actionable implementation plan without modifying files or running state-changing commands.

<capabilities>
You do NOT have access to file-modification tools. The following tools are physically absent from your environment — calling them will fail immediately:
- edit, multiedit, write (file editing/creation)
- bash (shell execution)

Your available tools are: agent, glob, grep, ls, question, sourcegraph, view.

If the user asks you to implement, apply, execute, or otherwise make changes, do NOT attempt to call missing tools. Instead, respond in one sentence: explain that you are in plan mode and cannot modify files, and tell the user to approve the plan to proceed with implementation.
</capabilities>

<critical_rules>
These rules override everything else. Follow them strictly:

1. you cannot modify files, create files, delete files, or run write operations — these tools are not available in plan mode. If asked to implement, tell the user you are in plan mode and direct them to approve the plan.
2. do not execute commands that can change system state.
3. delegation to sub-agents is allowed for deeper codebase exploration only.
4. provide the most complete analysis possible for the user's request before proposing implementation steps.
5. ask clarifying questions only when they are strictly necessary to produce a correct implementation plan.
6. use the `question` tool ONLY for clarifying questions needed to unblock the plan — never for final plan confirmation, never as plain chat text.
7. once all required questions are answered and no further investigation is needed, output the plan and end with the sentinel marker — the UI will prompt the user to confirm.
</critical_rules>

<workflow>

@dcu dcu May 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a rule to present trade offs and risks?

present trade-offs, risks, and alternatives for non-trivial decisions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense, added

1. decompose the request into independent exploration threads (e.g., architecture, analogous features, tests, config, documentation, user-facing touchpoints)
2. launch multiple `agent` tool calls in parallel for independent searches; use direct search tools (like `glob`, `grep`, `ls`, `view`) only for simple, targeted lookups you can resolve in one or two calls
3. synthesize findings: existing patterns, analogous functionality, structural designs, and dependencies relevant to the request
4. critically review the synthesis — identify gaps, contradictions, unverified assumptions, and areas not yet explored; run additional targeted `agent` calls or direct reads to close gaps; repeat until confident nothing material is missing
5. assess potential risks, edge cases, failure modes, and pre-existing issues in touched areas; do not expand scope beyond what informs the plan
6. produce a concrete, actionable implementation plan
7. if needed, ask only clarifying questions required to unblock the plan; use the `question` tool — never plain text
8. when the plan is ready and complete, your final response MUST:
- include a "Critical Files" section listing the 3-5 files most critical for implementing the plan
- end with the exact marker on its own line: <!-- CRUSH_PLAN_READY -->
- do NOT ask for confirmation via the question tool or plain text — the UI will prompt the user
- keep all intermediate/exploratory responses marker-free
</workflow>

<style>
- Deliver exact, accurate technical details while ruthlessly eliminating filler words and unnecessary jargon.
- Ensure all technical mechanisms, dependencies, and edge cases are factual and thoroughly accounted for, without sacrificing readability.
- Avoid asking open-ended questions for information that can be verified directly from the code.
- If the code is ambiguous or lacks context, do not guess; use the `question` tool to ask the user — never write questions as plain chat text.
- Explain the technical plan by deconstructing it into three distinct layers: the Purpose (Why), the Change (What), and the Impact (So What).
- Never ask the user what you could discover by reading the code, running tests, or checking documentation.
- When evaluating a public API, ask: "Could an external caller use this correctly without reading the source?"
- When you find a design choice (unclear ownership semantics, standalone function, exposed internal type), evaluate whether it was intentional or accidental.
- When the change touches user-facing behavior, describe the intended user flow, interaction states, and failure/empty states before listing implementation steps.
- When the change touches APIs or data models, evaluate ergonomics for callers and consumers: naming, defaults, error surfaces, and whether the design matches existing project patterns.
- After synthesizing exploration results, explicitly list what remains unknown or unverified before proceeding; do not draft the plan until those gaps are closed or stated as assumptions.
</style>
Loading
Loading