on main help sign in
refactor: agent tool
commitb45dcfe41fa16475d58eb8c4fd73a9e3829d6698 authorkujtimiihoxha <kujtimii.h@gmail.com> dateFri 3 Oct 2025, 10:42 (1 year ago) parent5401adf
30 files changed+127−99
.gitignore+1 go.mod+1−1 go.sum+6 internal/agent/agent.go+6−5 internal/agent/agent-tool.go → internal/agent/agent_tool.go+9−3 internal/agent/common_test.go−19 internal/agent/coordinator.go−2 internal/agent/prompt/prompt.go+5−5 internal/agent/prompts.go+2−2 internal/agent/templates/agentTool.md → internal/agent/templates/agent_tool.md+3−4 internal/agent/templates/coder.gotmpl → internal/agent/templates/coder.md.tpl+12−8 internal/agent/templates/initialize.md+1−1 internal/agent/templates/task.gotmpl → internal/agent/templates/task.md.tpl internal/agent/templates/title.md+2 internal/agent/tools/bash.go+1−1 internal/agent/tools/bash.gotmpl → internal/agent/tools/bash.tpl+1−1 internal/agent/tools/edit.md+4−7 internal/agent/tools/glob.md+2−4 internal/agent/tools/grep.md+4−7 internal/agent/tools/ls.md+1−2 internal/agent/tools/multiedit.md+3−6 internal/agent/tools/sourcegraph.md+2−4 internal/agent/tools/tools.go+13 internal/agent/tools/view.md+1−2 internal/agent/tools/write.md+1−2 internal/session/session.go+27 internal/tui/components/chat/chat.go+10−2 internal/tui/components/chat/sidebar/sidebar.go+7−7 internal/tui/components/dialogs/commands/commands.go+1−1 internal/tui/page/chat/chat.go+1−3
.gitignore+1
@@ -49,3 +49,4 @@ Thumbs.db
4949manpages/
5050completions/
5151!internal/tui/components/completions/
52+.prettierignore
go.mod+1−1
@@ -68,7 +68,7 @@ require (
6868
6969require (
7070 github.com/anthropics/anthropic-sdk-go v1.12.0 // indirect
71− github.com/charmbracelet/fantasy v0.0.0-20251002051643-c96822199d77
71+ github.com/charmbracelet/fantasy v0.0.0-20251003071236-5d39f0348e5d
7272 github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
7373)
7474
go.sum+6
@@ -53,6 +53,12 @@ github.com/charmbracelet/fang v0.4.2 h1:nWr7Tb82/TTNNGMGG35aTZ1X68loAOQmpb0qxkKX
5353github.com/charmbracelet/fang v0.4.2/go.mod h1:wHJKQYO5ReYsxx+yZl+skDtrlKO/4LLEQ6EXsdHhRhg=
5454github.com/charmbracelet/fantasy v0.0.0-20251002051643-c96822199d77 h1:YHuUqaojkeu00YtQeXPqM/1RNJH/jqGNaQYFwa7JQTk=
5555github.com/charmbracelet/fantasy v0.0.0-20251002051643-c96822199d77/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
56+github.com/charmbracelet/fantasy v0.0.0-20251003054629-33bf06cef92c h1:+gCA3Sv7g1jnZ96Em7j9u61H2O/1SkBAZ1LM9yd2bM8=
57+github.com/charmbracelet/fantasy v0.0.0-20251003054629-33bf06cef92c/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
58+github.com/charmbracelet/fantasy v0.0.0-20251003055851-3196e9fa7380 h1:UYWO3cutUTV9ZLOH4hrXFy9hg4E1pVbLZsZLA1OA3+g=
59+github.com/charmbracelet/fantasy v0.0.0-20251003055851-3196e9fa7380/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
60+github.com/charmbracelet/fantasy v0.0.0-20251003071236-5d39f0348e5d h1:HS9qu7CgQGPnPcNoHIZXGWGFS8SaxaBXYytLdakm+jY=
61+github.com/charmbracelet/fantasy v0.0.0-20251003071236-5d39f0348e5d/go.mod h1:RZotHpq44tKZDe6Vf0kk1iDqnUgH7Scx+K/7uJ9Qwnw=
5662github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 h1:PU4Zvpagsk5sgaDxn5W4sxHuLp9QRMBZB3bFSk40A4w=
5763github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018/go.mod h1:Z/GLmp9fzaqX4ze3nXG7StgWez5uBM5XtlLHK8V/qSk=
5864github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea h1:g1HfUgSMvye8mgecMD1mPscpt+pzJoDEiSA+p2QXzdQ=
internal/agent/agent.go+6−5
@@ -175,7 +175,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
175175 TopK: call.TopK,
176176 FrequencyPenalty: call.FrequencyPenalty,
177177 // Before each step create the new assistant message
178− PrepareStep: func(options ai.PrepareStepFunctionOptions) (prepared ai.PrepareStepResult, err error) {
178+ PrepareStep: func(callContext context.Context, options ai.PrepareStepFunctionOptions) (_ context.Context, prepared ai.PrepareStepResult, err error) {
179179 var assistantMsg message.Message
180180 assistantMsg, err = a.messages.Create(genCtx, call.SessionID, message.CreateMessageParams{
181181 Role: message.Assistant,
@@ -184,9 +184,11 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
184184 Provider: a.largeModel.ModelCfg.Provider,
185185 })
186186 if err != nil {
187− return prepared, err
187+ return callContext, prepared, err
188188 }
189189
190+ callContext = context.WithValue(ctx, tools.MessageIDContextKey, assistantMsg.ID)
191+
190192 currentAssistant = &assistantMsg
191193
192194 prepared.Messages = options.Messages
@@ -200,7 +202,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
200202 for _, queued := range queuedCalls {
201203 userMessage, createErr := a.createUserMessage(genCtx, queued)
202204 if createErr != nil {
203− return prepared, createErr
205+ return callContext, prepared, createErr
204206 }
205207 prepared.Messages = append(prepared.Messages, userMessage.ToAIMessage()...)
206208 }
@@ -220,7 +222,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
220222 prepared.Messages[i].ProviderOptions = a.getCacheControlOptions()
221223 }
222224 }
223− return prepared, err
225+ return callContext, prepared, err
224226 },
225227 OnReasoningDelta: func(id string, text string) error {
226228 currentAssistant.AppendReasoningContent(text)
@@ -307,7 +309,6 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*ai.Agen
307309 case ai.FinishReasonToolCalls:
308310 finishReason = message.FinishReasonToolUse
309311 }
310− slog.Info("OnStepFinish", "reason", stepResult.FinishReason)
311312 currentAssistant.AddFinish(finishReason, "", "")
312313 a.updateSessionUsage(a.largeModel, &currentSession, stepResult.Usage)
313314 sessionLock.Lock()
internal/agent/agent-tool.go → internal/agent/agent_tool.gorenamed+9−3
@@ -14,7 +14,7 @@ import (
1414 "github.com/charmbracelet/crush/internal/config"
1515)
1616
17−//go:embed templates/agentTool.md
17+//go:embed templates/agent_tool.md
1818var agentToolDescription []byte
1919
2020type AgentParams struct {
@@ -55,7 +55,13 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
5555 return ai.ToolResponse{}, errors.New("session id missing from context")
5656 }
5757
58− session, err := c.sessions.CreateTaskSession(ctx, call.ID, sessionID, "New Agent Session")
58+ agentMessageID := tools.GetMessageFromContext(ctx)
59+ if agentMessageID == "" {
60+ return ai.ToolResponse{}, errors.New("agent message id missing from context")
61+ }
62+
63+ agentToolSessionID := c.sessions.CreateAgentToolSessionID(agentMessageID, call.ID)
64+ session, err := c.sessions.CreateTaskSession(ctx, agentToolSessionID, sessionID, "New Agent Session")
5965 if err != nil {
6066 return ai.ToolResponse{}, fmt.Errorf("error creating session: %s", err)
6167 }
@@ -65,7 +71,7 @@ func (c *coordinator) agentTool() (ai.AgentTool, error) {
6571 maxTokens = model.ModelCfg.MaxTokens
6672 }
6773 result, err := agent.Run(ctx, SessionAgentCall{
68− SessionID: sessionID,
74+ SessionID: session.ID,
6975 Prompt: params.Prompt,
7076 MaxOutputTokens: maxTokens,
7177 ProviderOptions: c.getProviderOptions(model),
internal/agent/common_test.go−19
@@ -1,7 +1,6 @@
11package agent
22
33import (
4− "fmt"
54 "net/http"
65 "os"
76 "path/filepath"
@@ -69,18 +68,9 @@ func openaiBuilder(model string) builderFunc {
6968
7069func openRouterBuilder(model string) builderFunc {
7170 return func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
72− tf := func() func() string {
73− id := 0
74− return func() string {
75− id += 1
76− return fmt.Sprintf("%s-%d", t.Name(), id)
77− }
78− }
7971 provider := openrouter.New(
8072 openrouter.WithAPIKey(os.Getenv("CRUSH_OPENROUTER_API_KEY")),
8173 openrouter.WithHTTPClient(&http.Client{Transport: r}),
82− openrouter.WithLanguageUniqueToolCallIds(),
83− openrouter.WithLanguageModelGenerateIDFunc(tf()),
8474 )
8575 return provider.LanguageModel(model)
8676 }
@@ -88,19 +78,10 @@ func openRouterBuilder(model string) builderFunc {
8878
8979func zAIBuilder(model string) builderFunc {
9080 return func(t *testing.T, r *recorder.Recorder) (ai.LanguageModel, error) {
91− tf := func() func() string {
92− id := 0
93− return func() string {
94− id += 1
95− return fmt.Sprintf("%s-%d", t.Name(), id)
96− }
97− }
9881 provider := openaicompat.New(
9982 "https://api.z.ai/api/coding/paas/v4",
10083 openaicompat.WithAPIKey(os.Getenv("CRUSH_ZAI_API_KEY")),
10184 openaicompat.WithHTTPClient(&http.Client{Transport: r}),
102− openaicompat.WithLanguageUniqueToolCallIds(),
103− openaicompat.WithLanguageModelGenerateIDFunc(tf()),
10485 )
10586 return provider.LanguageModel(model)
10687 }
internal/agent/coordinator.go−2
@@ -356,7 +356,6 @@ func (c *coordinator) buildOpenaiProvider(baseURL, apiKey string, headers map[st
356356func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[string]string) ai.Provider {
357357 opts := []openrouter.Option{
358358 openrouter.WithAPIKey(apiKey),
359− openrouter.WithLanguageUniqueToolCallIds(),
360359 }
361360 if c.cfg.Options.Debug {
362361 httpClient := log.NewHTTPClient()
@@ -371,7 +370,6 @@ func (c *coordinator) buildOpenrouterProvider(_, apiKey string, headers map[stri
371370func (c *coordinator) buildOpenaiCompatProvider(baseURL, apiKey string, headers map[string]string) ai.Provider {
372371 opts := []openaicompat.Option{
373372 openaicompat.WithAPIKey(apiKey),
374− openaicompat.WithLanguageUniqueToolCallIds(),
375373 }
376374 if c.cfg.Options.Debug {
377375 httpClient := log.NewHTTPClient()
internal/agent/prompt/prompt.go+5−5
@@ -15,11 +15,11 @@ import (
1515
1616// Prompt represents a template-based prompt generator.
1717type Prompt struct {
18− name string
19− template string
20− now func() time.Time
21− platform string
22− workingDir string
18+ name string
19+ template string
20+ now func() time.Time
21+ platform string
22+ workingDir string
2323}
2424
2525type PromptDat struct {
internal/agent/prompts.go+2−2
@@ -6,10 +6,10 @@ import (
66 "github.com/charmbracelet/crush/internal/agent/prompt"
77)
88
9−//go:embed templates/coder.gotmpl
9+//go:embed templates/coder.md.tpl
1010var coderPromptTmpl []byte
1111
12−//go:embed templates/task.gotmpl
12+//go:embed templates/task.md.tpl
1313var taskPromptTmpl []byte
1414
1515//go:embed templates/initialize.md
internal/agent/templates/agentTool.md → internal/agent/templates/agent_tool.mdrenamed+3−4
@@ -1,16 +1,15 @@
11Launch a new agent that has access to the following tools: GlobTool, GrepTool, LS, View. When you are searching for a keyword or file and are not confident that you will find the right match on the first try, use the Agent tool to perform the search for you.
2−<usage>
32
3+<usage>
44- If you are searching for a keyword like "config" or "logger", or for questions like "which file does X?", the Agent tool is strongly recommended
55- If you want to read a specific file path, use the View or GlobTool tool instead of the Agent tool, to find the match more quickly
66- If you are searching for a specific class definition like "class Foo", use the GlobTool tool instead, to find the match more quickly
7− </usage>
7+</usage>
88
99<usage_notes>
10−
11101. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
12112. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
13123. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
14134. The agent's outputs should generally be trusted
15145. IMPORTANT: The agent can not use Bash, Replace, Edit, so can not modify files. If you want to use these tools, use them directly instead of going through the agent.
16− </usage_notes>
15+</usage_notes>
internal/agent/templates/coder.gotmpl → internal/agent/templates/coder.md.tplrenamed+12−8
@@ -2,18 +2,19 @@ You are Crush, a powerful AI Assistant that runs in the CLI.
22Use the instructions below and the tools available to you to assist the user.
33
44<memory_instructions>
5−If the current working directory contains a file called CRUSH.md, it will be automatically added to your context.
5+If the current working directory contains a file used for memory, they will be automatically added to your context.
66
7−This file serves multiple purposes:
7+These file serves multiple purposes:
88
99- Storing frequently used bash commands (build, test, lint, etc.) so you can use them without searching each time
1010- Recording the user's code style preferences (naming conventions, preferred libraries, etc.)
1111- Maintaining useful information about the codebase structure and organization
1212
13−When you discover important information that could be useful for the future update/add the info to the CRUSH.md.
13+When you discover important information that could be useful for the future update/add the info the appropriate memory file.
1414
15−Memory might be added to you during a task if there are nested memory files that relate to the work you are doing.
15+Make sure to follow the memory files instructions while working.
1616</memory_instructions>
17+
1718<communication_style>
1819- Be concise and direct
1920- Keep responses under 4 lines unless details requested
@@ -65,6 +66,7 @@ assistant: src/foo.c
6566user: write tests for new feature
6667assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit file tool to write new tests]
6768</example>
69+
6870</communication_style>
6971
7072<proactiveness>
@@ -86,15 +88,17 @@ When making changes to files, first understand the file's code conventions. Mimi
8688</following_conversations>
8789
8890<code_style>
89−- Follow existing code style and patterns
90−- Do not add any comments to code you write unless asked to do so
91+- Follow existing code style and patterns.
92+- Do not add any comments to code you write unless asked to do so.
93+- Thrive to write only code that is necessary to solve the given issue (less code is always better).
94+- Follow best practices for the language and framework used in the project.
9195</code_style>
9296
9397<doing_tasks>
9498The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
9599
96100- Use the available search tools to understand the codebase and the user's query.
97−- Plan out the implementation
101+- Plan out the implementation (create a todo list)
98102- Implement the solution using all tools available to you
99103- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
100104- When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to CRUSH.md so that you will know to run it next time.
@@ -103,7 +107,7 @@ NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTAN
103107</doing_tasks>
104108
105109<tool_use>
106−- When doing file search, prefer to use the Agent tool in order to reduce context usage.
110+- When doing file search, prefer to use the Agent tool, give the agent detailed instructions on what to search for and response format details.
107111- All tools are executed in parallel when multiple tool calls are sent in a single message. Only send multiple tool calls when they are safe to run in parallel (no dependencies between them).
108112- The user does not see the full output of the tool responses, so if you need the output of the tool for the response make sure to summarize it for the user.
109113</tool_use>
internal/agent/templates/initialize.md+1−1
@@ -1,4 +1,4 @@
1−`Please analyze this codebase and create a **CRUSH.md** file containing:
1+Please analyze this codebase and create a **CRUSH.md** file containing:
22
33- Build/lint/test commands - especially for running a single test
44- Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
internal/agent/templates/task.gotmpl → internal/agent/templates/task.md.tplrenamed

The content is the same.

internal/agent/templates/title.md+2
@@ -1,8 +1,10 @@
11you will generate a short title based on the first message a user begins a conversation with
22
3+<rules>
34- ensure it is not more than 50 characters long
45- the title should be a summary of the user's message
56- it should be one line long
67- do not use quotes or colons
78- the entire text you return will be used as the title
89- never return anything that is more than one sentence (one line) long
10+</rules>
internal/agent/tools/bash.go+1−1
@@ -44,7 +44,7 @@ const (
4444 BashNoOutput = "no output"
4545)
4646
47−//go:embed bash.gotmpl
47+//go:embed bash.tpl
4848var bashDescriptionTmpl []byte
4949
5050var bashDescriptionTpl = template.Must(
internal/agent/tools/bash.gotmpl → internal/agent/tools/bash.tplrenamed+1−1
@@ -108,7 +108,7 @@ Important:
108108
109109- Return empty response - user sees gh output
110110- Never update git config
111− </pull_requests>
111+</pull_requests>
112112
113113<examples>
114114Good: pytest /foo/bar/tests
internal/agent/tools/edit.md+4−7
@@ -13,10 +13,9 @@ Edits files by replacing text, creating new files, or deleting content. For movi
1313</parameters>
1414
1515<special_cases>
16−
1716- Create file: provide file_path + new_string, leave old_string empty
1817- Delete content: provide file_path + old_string, leave new_string empty
19− </special_cases>
18+</special_cases>
2019
2120<critical_requirements>
2221UNIQUENESS (when replace_all=false): old_string MUST uniquely identify target instance
@@ -34,7 +33,7 @@ VERIFICATION: Before using
3433- Check how many instances of target text exist
3534- Gather sufficient context for unique identification
3635- Plan separate calls or use replace_all
37− </critical_requirements>
36+</critical_requirements>
3837
3938<warnings>
4039Tool fails if:
@@ -44,17 +43,15 @@ Tool fails if:
4443</warnings>
4544
4645<best_practices>
47−
4846- Ensure edits result in correct, idiomatic code
4947- Don't leave code in broken state
5048- Use absolute file paths (starting with /)
5149- Use forward slashes (/) for cross-platform compatibility
5250- Multiple edits to same file: send all in single message with multiple tool calls
53− </best_practices>
51+</best_practices>
5452
5553<windows_notes>
56−
5754- Forward slashes work throughout (C:/path/file)
5855- File permissions handled automatically
5956- Line endings converted automatically (\n ↔ \r\n)
60− </windows_notes>
57+</windows_notes>
internal/agent/tools/glob.md+2−4
@@ -7,13 +7,12 @@ Fast file pattern matching tool that finds files by name/pattern, returning path
77</usage>
88
99<pattern_syntax>
10−
1110- '\*' matches any sequence of non-separator characters
1211- '\*\*' matches any sequence including separators
1312- '?' matches any single non-separator character
1413- '[...]' matches any character in brackets
1514- '[!...]' matches any character not in brackets
16− </pattern_syntax>
15+</pattern_syntax>
1716
1817<examples>
1918- '*.js' - JavaScript files in current directory
@@ -29,11 +28,10 @@ Fast file pattern matching tool that finds files by name/pattern, returning path
2928</limitations>
3029
3130<cross_platform>
32−
3331- Path separators handled automatically (/ and \ work)
3432- Uses ripgrep (rg) if available, otherwise Go implementation
3533- Patterns should use forward slashes (/) for compatibility
36− </cross_platform>
34+</cross_platform>
3735
3836<tips>
3937- Combine with Grep: find files with Glob, search contents with Grep
internal/agent/tools/grep.md+4−7
@@ -14,14 +14,13 @@ When literal_text=false (supports standard regex):
1414- 'function' searches for literal text "function"
1515- 'log\..\*Error' finds text starting with "log." and ending with "Error"
1616- 'import\s+.\*\s+from' finds import statements in JavaScript/TypeScript
17− </regex_syntax>
17+</regex_syntax>
1818
1919<include_patterns>
20−
2120- '\*.js' - Only search JavaScript files
2221- '\*.{ts,tsx}' - Only search TypeScript files
2322- '\*.go' - Only search Go files
24− </include_patterns>
23+</include_patterns>
2524
2625<limitations>
2726- Results limited to 100 files (newest first)
@@ -31,18 +30,16 @@ When literal_text=false (supports standard regex):
3130</limitations>
3231
3332<ignore_support>
34−
3533- Respects .gitignore patterns to skip ignored files/directories
3634- Respects .crushignore patterns for additional ignore rules
3735- Both ignore files auto-detected in search root directory
38− </ignore_support>
36+</ignore_support>
3937
4038<cross_platform>
41−
4239- Uses ripgrep (rg) if available for better performance
4340- Falls back to Go implementation if ripgrep unavailable
4441- File paths normalized automatically for compatibility
45− </cross_platform>
42+</cross_platform>
4643
4744<tips>
4845- For faster searches: use Glob to find relevant files first, then Grep
internal/agent/tools/ls.md+1−2
@@ -21,12 +21,11 @@ Shows files and subdirectories in tree structure for exploring project organizat
2121</limitations>
2222
2323<cross_platform>
24−
2524- Hidden file detection uses Unix convention (files starting with '.')
2625- Windows hidden files (with hidden attribute) not auto-skipped
2726- Common Windows directories (System32, Program Files) not in default ignore
2827- Path separators handled automatically (/ and \ work)
29− </cross_platform>
28+</cross_platform>
3029
3130<tips>
3231- Use Glob for finding files by name patterns instead of browsing
internal/agent/tools/multiedit.md+3−6
@@ -21,11 +21,10 @@ Makes multiple edits to a single file in one operation. Built on Edit tool for e
2121</operation>
2222
2323<critical_requirements>
24−
25241. All edits follow same requirements as single Edit tool
26252. Edits are atomic - either all succeed or none applied
27263. Plan edits carefully to avoid conflicts between sequential operations
28− </critical_requirements>
27+</critical_requirements>
2928
3029<warnings>
3130- Tool fails if old_string doesn't match file contents exactly (including whitespace)
@@ -34,17 +33,15 @@ Makes multiple edits to a single file in one operation. Built on Edit tool for e
3433</warnings>
3534
3635<best_practices>
37−
3836- Ensure all edits result in correct, idiomatic code
3937- Don't leave code in broken state
4038- Use absolute file paths (starting with /)
4139- Use replace_all for renaming variables across file
4240- Avoid adding emojis unless user explicitly requests
43− </best_practices>
41+</best_practices>
4442
4543<new_file_creation>
46−
4744- Provide new file path (including directory if needed)
4845- First edit: empty old_string, new file contents as new_string
4946- Subsequent edits: normal edit operations on created content
50− </new_file_creation>
47+</new_file_creation>
internal/agent/tools/sourcegraph.md+2−4
@@ -7,7 +7,6 @@ Search code across public repositories using Sourcegraph's GraphQL API.
77</usage>
88
99<basic_syntax>
10−
1110- "fmt.Println" - exact matches
1211- "file:.go fmt.Println" - limit to Go files
1312- "repo:^github\.com/golang/go$ fmt.Println" - specific repos
@@ -16,7 +15,7 @@ Search code across public repositories using Sourcegraph's GraphQL API.
1615- "fmt\.(Print|Printf|Println)" - regex patterns
1716- "\"exact phrase\"" - exact phrase matching
1817- "-file:test" or "-repo:forks" - exclude matches
19− </basic_syntax>
18+</basic_syntax>
2019
2120<key_filters>
2221Repository: repo:name, repo:^exact$, repo:org/repo@branch, -repo:exclude, fork:yes, archived:yes, visibility:public
@@ -35,12 +34,11 @@ Result: select:repo, select:file, select:content, count:100, timeout:30s
3534</examples>
3635
3736<boolean_operators>
38−
3937- "term1 AND term2" - both terms
4038- "term1 OR term2" - either term
4139- "term1 NOT term2" - term1 but not term2
4240- "term1 and (term2 or term3)" - grouping with parentheses
43− </boolean_operators>
41+</boolean_operators>
4442
4543<limitations>
4644- Only searches public repositories
internal/agent/tools/tools.go+13
@@ -11,6 +11,7 @@ type (
1111
1212const (
1313 SessionIDContextKey sessionIDContextKey = "session_id"
14+ MessageIDContextKey messageIDContextKey = "message_id"
1415)
1516
1617func GetSessionFromContext(ctx context.Context) string {
@@ -24,3 +25,15 @@ func GetSessionFromContext(ctx context.Context) string {
2425 }
2526 return s
2627}
28+
29+func GetMessageFromContext(ctx context.Context) string {
30+ messageID := ctx.Value(MessageIDContextKey)
31+ if messageID == nil {
32+ return ""
33+ }
34+ s, ok := messageID.(string)
35+ if !ok {
36+ return ""
37+ }
38+ return s
39+}
internal/agent/tools/view.md+1−2
@@ -23,11 +23,10 @@ Reads and displays file contents with line numbers for examining code, logs, or
2323</limitations>
2424
2525<cross_platform>
26−
2726- Handles Windows (CRLF) and Unix (LF) line endings
2827- Works with forward slashes (/) and backslashes (\)
2928- Auto-detects text encoding for common formats
30− </cross_platform>
29+</cross_platform>
3130
3231<tips>
3332- Use with Glob to find files first
internal/agent/tools/write.md+1−2
@@ -19,9 +19,8 @@ Creates or updates files in filesystem for saving/modifying text content.
1919</limitations>
2020
2121<cross_platform>
22−
2322- Use forward slashes (/) for compatibility
24− </cross_platform>
23+</cross_platform>
2524
2625<tips>
2726- Use View tool first to examine existing files before modifying
internal/session/session.go+27
@@ -3,6 +3,8 @@ package session
33import (
44 "context"
55 "database/sql"
6+ "fmt"
7+ "strings"
68
79 "github.com/charmbracelet/crush/internal/db"
810 "github.com/charmbracelet/crush/internal/event"
@@ -32,6 +34,11 @@ type Service interface {
3234 List(ctx context.Context) ([]Session, error)
3335 Save(ctx context.Context, session Session) (Session, error)
3436 Delete(ctx context.Context, id string) error
37+
38+ // Agent tool session management
39+ CreateAgentToolSessionID(messageID, toolCallID string) string
40+ ParseAgentToolSessionID(sessionID string) (messageID string, toolCallID string, ok bool)
41+ IsAgentToolSession(sessionID string) bool
3542}
3643
3744type service struct {
@@ -157,3 +164,23 @@ func NewService(q db.Querier) Service {
157164 q,
158165 }
159166}
167+
168+// CreateAgentToolSessionID creates a session ID for agent tool sessions using the format "messageID$$toolCallID"
169+func (s *service) CreateAgentToolSessionID(messageID, toolCallID string) string {
170+ return fmt.Sprintf("%s$$%s", messageID, toolCallID)
171+}
172+
173+// ParseAgentToolSessionID parses an agent tool session ID into its components
174+func (s *service) ParseAgentToolSessionID(sessionID string) (messageID string, toolCallID string, ok bool) {
175+ parts := strings.Split(sessionID, "$$")
176+ if len(parts) != 2 {
177+ return "", "", false
178+ }
179+ return parts[0], parts[1], true
180+}
181+
182+// IsAgentToolSession checks if a session ID follows the agent tool session format
183+func (s *service) IsAgentToolSession(sessionID string) bool {
184+ _, _, ok := s.ParseAgentToolSessionID(sessionID)
185+ return ok
186+}
internal/tui/components/chat/chat.go+10−2
@@ -261,12 +261,19 @@ func (m *messageListCmp) handleChildSession(event pubsub.Event[message.Message])
261261 if len(event.Payload.ToolCalls()) == 0 && len(event.Payload.ToolResults()) == 0 {
262262 return nil
263263 }
264+
265+ // Check if this is an agent tool session and parse it
266+ childSessionID := event.Payload.SessionID
267+ parentMessageID, toolCallID, ok := m.app.Sessions.ParseAgentToolSessionID(childSessionID)
268+ if !ok {
269+ return nil
270+ }
264271 items := m.listCmp.Items()
265272 toolCallInx := NotFound
266273 var toolCall messages.ToolCallCmp
267274 for i := len(items) - 1; i >= 0; i-- {
268275 if msg, ok := items[i].(messages.ToolCallCmp); ok {
269− if msg.GetToolCall().ID == event.Payload.SessionID {
276+ if msg.ParentMessageID() == parentMessageID && msg.GetToolCall().ID == toolCallID {
270277 toolCallInx = i
271278 toolCall = msg
272279 }
@@ -613,7 +620,8 @@ func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResult
613620 uiMessages = append(uiMessages, messages.NewToolCallCmp(msg.ID, tc, m.app.Permissions, options...))
614621 // If this tool call is the agent tool, fetch nested tool calls
615622 if tc.Name == agent.AgentToolName {
616− nestedMessages, _ := m.app.Messages.List(context.Background(), tc.ID)
623+ agentToolSessionID := m.app.Sessions.CreateAgentToolSessionID(msg.ID, tc.ID)
624+ nestedMessages, _ := m.app.Messages.List(context.Background(), agentToolSessionID)
617625 nestedToolResultMap := m.buildToolResultMap(nestedMessages)
618626 nestedUIMessages := m.convertMessagesToUI(nestedMessages, nestedToolResultMap)
619627 nestedToolCalls := make([]messages.ToolCallCmp, 0, len(nestedUIMessages))
internal/tui/components/chat/sidebar/sidebar.go+7−7
@@ -563,13 +563,6 @@ func (s *sidebarCmp) currentModelBlock() string {
563563 if model.CanReason {
564564 reasoningInfoStyle := t.S().Subtle.PaddingLeft(2)
565565 switch modelProvider.Type {
566− case catwalk.TypeOpenAI:
567− reasoningEffort := model.DefaultReasoningEffort
568− if selectedModel.ReasoningEffort != "" {
569− reasoningEffort = selectedModel.ReasoningEffort
570− }
571− formatter := cases.Title(language.English, cases.NoLower)
572− parts = append(parts, reasoningInfoStyle.Render(formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort))))
573566 case catwalk.TypeAnthropic:
574567 formatter := cases.Title(language.English, cases.NoLower)
575568 if selectedModel.Think {
@@ -577,6 +570,13 @@ func (s *sidebarCmp) currentModelBlock() string {
577570 } else {
578571 parts = append(parts, reasoningInfoStyle.Render(formatter.String("Thinking off")))
579572 }
573+ default:
574+ reasoningEffort := model.DefaultReasoningEffort
575+ if selectedModel.ReasoningEffort != "" {
576+ reasoningEffort = selectedModel.ReasoningEffort
577+ }
578+ formatter := cases.Title(language.English, cases.NoLower)
579+ parts = append(parts, reasoningInfoStyle.Render(formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort))))
580580 }
581581 }
582582 if s.session.ID != "" {
internal/tui/components/dialogs/commands/commands.go+1−1
@@ -326,7 +326,7 @@ func (c *commandDialogCmp) defaultCommands() []Command {
326326 }
327327
328328 // OpenAI models: reasoning effort dialog
329− if providerCfg.Type == catwalk.TypeOpenAI && model.HasReasoningEffort {
329+ if model.HasReasoningEffort {
330330 commands = append(commands, Command{
331331 ID: "select_reasoning_effort",
332332 Title: "Select Reasoning Effort",
internal/tui/page/chat/chat.go+1−3
@@ -9,7 +9,6 @@ import (
99 "github.com/charmbracelet/bubbles/v2/key"
1010 "github.com/charmbracelet/bubbles/v2/spinner"
1111 tea "github.com/charmbracelet/bubbletea/v2"
12− "github.com/charmbracelet/catwalk/pkg/catwalk"
1312 "github.com/charmbracelet/crush/internal/app"
1413 "github.com/charmbracelet/crush/internal/config"
1514 "github.com/charmbracelet/crush/internal/history"
@@ -563,8 +562,7 @@ func (p *chatPage) openReasoningDialog() tea.Cmd {
563562 model := cfg.GetModelByType(agentCfg.Model)
564563 providerCfg := cfg.GetProviderForModel(agentCfg.Model)
565564
566− if providerCfg != nil && model != nil &&
567− providerCfg.Type == catwalk.TypeOpenAI && model.HasReasoningEffort {
565+ if providerCfg != nil && model != nil && model.HasReasoningEffort {
568566 // Return the OpenDialogMsg directly so it bubbles up to the main TUI
569567 return dialogs.OpenDialogMsg{
570568 Model: reasoning.NewReasoningDialog(),