on main help sign in
initial tool call stream
2de5127Kujtim Hoxha
internal/llm/agent/agent.go+22
@@ -380,6 +380,21 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
380 case provider.EventContentDelta:
380 case provider.EventContentDelta:
381 assistantMsg.AppendContent(event.Content)
381 assistantMsg.AppendContent(event.Content)
382 return a.messages.Update(ctx, *assistantMsg)
382 return a.messages.Update(ctx, *assistantMsg)
383+ case provider.EventToolUseStart:
384+ assistantMsg.AddToolCall(*event.ToolCall)
385+ return a.messages.Update(ctx, *assistantMsg)
386+ // TODO: see how to handle this
387+ // case provider.EventToolUseDelta:
388+ // tm := time.Unix(assistantMsg.UpdatedAt, 0)
389+ // assistantMsg.AppendToolCallInput(event.ToolCall.ID, event.ToolCall.Input)
390+ // if time.Since(tm) > 1000*time.Millisecond {
391+ // err := a.messages.Update(ctx, *assistantMsg)
392+ // assistantMsg.UpdatedAt = time.Now().Unix()
393+ // return err
394+ // }
395+ case provider.EventToolUseStop:
396+ assistantMsg.FinishToolCall(event.ToolCall.ID)
397+ return a.messages.Update(ctx, *assistantMsg)
383 case provider.EventError:
398 case provider.EventError:
384 if errors.Is(event.Error, context.Canceled) {
399 if errors.Is(event.Error, context.Canceled) {
385 logging.InfoPersist(fmt.Sprintf("Event processing canceled for session: %s", sessionID))
400 logging.InfoPersist(fmt.Sprintf("Event processing canceled for session: %s", sessionID))
@@ -456,6 +471,13 @@ func createAgentProvider(agentName config.AgentName) (provider.Provider, error)
456 provider.WithReasoningEffort(agentConfig.ReasoningEffort),
471 provider.WithReasoningEffort(agentConfig.ReasoningEffort),
457 ),
472 ),
458 )
473 )
474+ } else if model.Provider == models.ProviderAnthropic && model.CanReason {
475+ opts = append(
476+ opts,
477+ provider.WithAnthropicOptions(
478+ provider.WithAnthropicShouldThinkFn(provider.DefaultShouldThinkFn),
479+ ),
480+ )
459 }
481 }
460 agentProvider, err := provider.NewProvider(
482 agentProvider, err := provider.NewProvider(
461 model.Provider,
483 model.Provider,
internal/llm/provider/anthropic.go+46−14
@@ -93,8 +93,7 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
93 }
93 }
94
94
95 if len(blocks) == 0 {
95 if len(blocks) == 0 {
96− logging.Warn("There is a message without content, investigate")
96+ logging.Warn("There is a message without content, investigate, this should not happen")
97− // This should never happend but we log this because we might have a bug in our cleanup method
98 continue
97 continue
99 }
98 }
100 anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...))
99 anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...))
@@ -196,8 +195,8 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
196 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
195 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
197 cfg := config.Get()
196 cfg := config.Get()
198 if cfg.Debug {
197 if cfg.Debug {
199− jsonData, _ := json.Marshal(preparedMessages)
198+ // jsonData, _ := json.Marshal(preparedMessages)
200− logging.Debug("Prepared messages", "messages", string(jsonData))
199+ // logging.Debug("Prepared messages", "messages", string(jsonData))
201 }
200 }
202 attempts := 0
201 attempts := 0
203 for {
202 for {
@@ -243,8 +242,8 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
243 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
242 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
244 cfg := config.Get()
243 cfg := config.Get()
245 if cfg.Debug {
244 if cfg.Debug {
246− jsonData, _ := json.Marshal(preparedMessages)
245+ // jsonData, _ := json.Marshal(preparedMessages)
247− logging.Debug("Prepared messages", "messages", string(jsonData))
246+ // logging.Debug("Prepared messages", "messages", string(jsonData))
248 }
247 }
249 attempts := 0
248 attempts := 0
250 eventChan := make(chan ProviderEvent)
249 eventChan := make(chan ProviderEvent)
@@ -257,6 +256,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
257 )
256 )
258 accumulatedMessage := anthropic.Message{}
257 accumulatedMessage := anthropic.Message{}
259
258
259+ currentToolCallID := ""
260 for anthropicStream.Next() {
260 for anthropicStream.Next() {
261 event := anthropicStream.Current()
261 event := anthropicStream.Current()
262 err := accumulatedMessage.Accumulate(event)
262 err := accumulatedMessage.Accumulate(event)
@@ -267,7 +267,19 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
267
267
268 switch event := event.AsAny().(type) {
268 switch event := event.AsAny().(type) {
269 case anthropic.ContentBlockStartEvent:
269 case anthropic.ContentBlockStartEvent:
270+ if event.ContentBlock.Type == "text" {
270− eventChan <- ProviderEvent{Type: EventContentStart}
271+ eventChan <- ProviderEvent{Type: EventContentStart}
272+ } else if event.ContentBlock.Type == "tool_use" {
273+ currentToolCallID = event.ContentBlock.ID
274+ eventChan <- ProviderEvent{
275+ Type: EventToolUseStart,
276+ ToolCall: &message.ToolCall{
277+ ID: event.ContentBlock.ID,
278+ Name: event.ContentBlock.Name,
279+ Finished: false,
280+ },
281+ }
282+ }
271
283
272 case anthropic.ContentBlockDeltaEvent:
284 case anthropic.ContentBlockDeltaEvent:
273 if event.Delta.Type == "thinking_delta" && event.Delta.Thinking != "" {
285 if event.Delta.Type == "thinking_delta" && event.Delta.Thinking != "" {
@@ -280,11 +292,30 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
280 Type: EventContentDelta,
292 Type: EventContentDelta,
281 Content: event.Delta.Text,
293 Content: event.Delta.Text,
282 }
294 }
295+ } else if event.Delta.Type == "input_json_delta" {
296+ if currentToolCallID != "" {
297+ eventChan <- ProviderEvent{
298+ Type: EventToolUseDelta,
299+ ToolCall: &message.ToolCall{
300+ ID: currentToolCallID,
301+ Finished: false,
302+ Input: event.Delta.JSON.PartialJSON.Raw(),
303+ },
304+ }
305+ }
283 }
306 }
284− // TODO: check if we can somehow stream tool calls
285−
286 case anthropic.ContentBlockStopEvent:
307 case anthropic.ContentBlockStopEvent:
308+ if currentToolCallID != "" {
309+ eventChan <- ProviderEvent{
310+ Type: EventToolUseStop,
311+ ToolCall: &message.ToolCall{
312+ ID: currentToolCallID,
313+ },
314+ }
315+ currentToolCallID = ""
316+ } else {
287− eventChan <- ProviderEvent{Type: EventContentStop}
317+ eventChan <- ProviderEvent{Type: EventContentStop}
318+ }
288
319
289 case anthropic.MessageStopEvent:
320 case anthropic.MessageStopEvent:
290 content := ""
321 content := ""
@@ -378,10 +409,11 @@ func (a *anthropicClient) toolCalls(msg anthropic.Message) []message.ToolCall {
378 switch variant := block.AsAny().(type) {
409 switch variant := block.AsAny().(type) {
379 case anthropic.ToolUseBlock:
410 case anthropic.ToolUseBlock:
380 toolCall := message.ToolCall{
411 toolCall := message.ToolCall{
412+ ID: variant.ID,
413+ Name: variant.Name,
381− ID: variant.ID,
414+ Input: string(variant.Input),
382− Name: variant.Name,
415+ Type: string(variant.Type),
383− Input: string(variant.Input),
416+ Finished: true,
384− Type: string(variant.Type),
385 }
417 }
386 toolCalls = append(toolCalls, toolCall)
418 toolCalls = append(toolCalls, toolCall)
387 }
419 }
internal/llm/provider/openai.go+5−4
@@ -344,10 +344,11 @@ func (o *openaiClient) toolCalls(completion openai.ChatCompletion) []message.Too
344 if len(completion.Choices) > 0 && len(completion.Choices[0].Message.ToolCalls) > 0 {
344 if len(completion.Choices) > 0 && len(completion.Choices[0].Message.ToolCalls) > 0 {
345 for _, call := range completion.Choices[0].Message.ToolCalls {
345 for _, call := range completion.Choices[0].Message.ToolCalls {
346 toolCall := message.ToolCall{
346 toolCall := message.ToolCall{
347− ID: call.ID,
347+ ID: call.ID,
348− Name: call.Function.Name,
348+ Name: call.Function.Name,
349− Input: call.Function.Arguments,
349+ Input: call.Function.Arguments,
350− Type: "function",
350+ Type: "function",
351+ Finished: true,
351 }
352 }
352 toolCalls = append(toolCalls, toolCall)
353 toolCalls = append(toolCalls, toolCall)
353 }
354 }
internal/llm/provider/provider.go+5−2
@@ -15,6 +15,9 @@ const maxRetries = 8
15
15
16const (
16const (
17 EventContentStart EventType = "content_start"
17 EventContentStart EventType = "content_start"
18+ EventToolUseStart EventType = "tool_use_start"
19+ EventToolUseDelta EventType = "tool_use_delta"
20+ EventToolUseStop EventType = "tool_use_stop"
18 EventContentDelta EventType = "content_delta"
21 EventContentDelta EventType = "content_delta"
19 EventThinkingDelta EventType = "thinking_delta"
22 EventThinkingDelta EventType = "thinking_delta"
20 EventContentStop EventType = "content_stop"
23 EventContentStop EventType = "content_stop"
@@ -43,8 +46,8 @@ type ProviderEvent struct {
43 Content string
46 Content string
44 Thinking string
47 Thinking string
45 Response *ProviderResponse
48 Response *ProviderResponse
46−
49+ ToolCall *message.ToolCall
47− Error error
50+ Error error
48}
51}
49type Provider interface {
52type Provider interface {
50 SendMessages(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error)
53 SendMessages(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error)
internal/message/content.go+43
@@ -233,6 +233,40 @@ func (m *Message) AppendReasoningContent(delta string) {
233 }
233 }
234}
234}
235
235
236+func (m *Message) FinishToolCall(toolCallID string) {
237+ for i, part := range m.Parts {
238+ if c, ok := part.(ToolCall); ok {
239+ if c.ID == toolCallID {
240+ m.Parts[i] = ToolCall{
241+ ID: c.ID,
242+ Name: c.Name,
243+ Input: c.Input,
244+ Type: c.Type,
245+ Finished: true,
246+ }
247+ return
248+ }
249+ }
250+ }
251+}
252+
253+func (m *Message) AppendToolCallInput(toolCallID string, inputDelta string) {
254+ for i, part := range m.Parts {
255+ if c, ok := part.(ToolCall); ok {
256+ if c.ID == toolCallID {
257+ m.Parts[i] = ToolCall{
258+ ID: c.ID,
259+ Name: c.Name,
260+ Input: c.Input + inputDelta,
261+ Type: c.Type,
262+ Finished: c.Finished,
263+ }
264+ return
265+ }
266+ }
267+ }
268+}
269+
236func (m *Message) AddToolCall(tc ToolCall) {
270func (m *Message) AddToolCall(tc ToolCall) {
237 for i, part := range m.Parts {
271 for i, part := range m.Parts {
238 if c, ok := part.(ToolCall); ok {
272 if c, ok := part.(ToolCall); ok {
@@ -246,6 +280,15 @@ func (m *Message) AddToolCall(tc ToolCall) {
246}
280}
247
281
248func (m *Message) SetToolCalls(tc []ToolCall) {
282func (m *Message) SetToolCalls(tc []ToolCall) {
283+ // remove any existing tool call part it could have multiple
284+ parts := make([]ContentPart, 0)
285+ for _, part := range m.Parts {
286+ if _, ok := part.(ToolCall); ok {
287+ continue
288+ }
289+ parts = append(parts, part)
290+ }
291+ m.Parts = parts
249 for _, toolCall := range tc {
292 for _, toolCall := range tc {
250 m.Parts = append(m.Parts, toolCall)
293 m.Parts = append(m.Parts, toolCall)
251 }
294 }
internal/message/message.go+2
@@ -5,6 +5,7 @@ import (
5 "database/sql"
5 "database/sql"
6 "encoding/json"
6 "encoding/json"
7 "fmt"
7 "fmt"
8+ "time"
8
9
9 "github.com/google/uuid"
10 "github.com/google/uuid"
10 "github.com/kujtimiihoxha/opencode/internal/db"
11 "github.com/kujtimiihoxha/opencode/internal/db"
@@ -116,6 +117,7 @@ func (s *service) Update(ctx context.Context, message Message) error {
116 if err != nil {
117 if err != nil {
117 return err
118 return err
118 }
119 }
120+ message.UpdatedAt = time.Now().Unix()
119 s.Publish(pubsub.UpdatedEvent, message)
121 s.Publish(pubsub.UpdatedEvent, message)
120 return nil
122 return nil
121}
123}
internal/pubsub/broker.go−7
@@ -7,13 +7,6 @@ import (
7
7
8const bufferSize = 1024
8const bufferSize = 1024
9
9
10−type Logger interface {
11− Debug(msg string, args ...any)
12− Info(msg string, args ...any)
13− Warn(msg string, args ...any)
14− Error(msg string, args ...any)
15−}
16−
17// Broker allows clients to publish events and subscribe to events
10// Broker allows clients to publish events and subscribe to events
18type Broker[T any] struct {
11type Broker[T any] struct {
19 subs map[chan Event[T]]struct{} // subscriptions
12 subs map[chan Event[T]]struct{} // subscriptions
internal/tui/components/chat/list.go+26−91
@@ -4,8 +4,6 @@ import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6 "math"
6 "math"
7− "sync"
8− "time"
9
7
10 "github.com/charmbracelet/bubbles/key"
8 "github.com/charmbracelet/bubbles/key"
11 "github.com/charmbracelet/bubbles/spinner"
9 "github.com/charmbracelet/bubbles/spinner"
@@ -13,7 +11,6 @@ import (
13 tea "github.com/charmbracelet/bubbletea"
11 tea "github.com/charmbracelet/bubbletea"
14 "github.com/charmbracelet/lipgloss"
12 "github.com/charmbracelet/lipgloss"
15 "github.com/kujtimiihoxha/opencode/internal/app"
13 "github.com/kujtimiihoxha/opencode/internal/app"
16− "github.com/kujtimiihoxha/opencode/internal/logging"
17 "github.com/kujtimiihoxha/opencode/internal/message"
14 "github.com/kujtimiihoxha/opencode/internal/message"
18 "github.com/kujtimiihoxha/opencode/internal/pubsub"
15 "github.com/kujtimiihoxha/opencode/internal/pubsub"
19 "github.com/kujtimiihoxha/opencode/internal/session"
16 "github.com/kujtimiihoxha/opencode/internal/session"
@@ -35,89 +32,14 @@ type messagesCmp struct {
35 messages []message.Message
32 messages []message.Message
36 uiMessages []uiMessage
33 uiMessages []uiMessage
37 currentMsgID string
34 currentMsgID string
38− mutex sync.Mutex
39 cachedContent map[string]cacheItem
35 cachedContent map[string]cacheItem
40 spinner spinner.Model
36 spinner spinner.Model
41− lastUpdate time.Time
42 rendering bool
37 rendering bool
43}
38}
44type renderFinishedMsg struct{}
39type renderFinishedMsg struct{}
45
40
46func (m *messagesCmp) Init() tea.Cmd {
41func (m *messagesCmp) Init() tea.Cmd {
47− return tea.Batch(m.viewport.Init())
42+ return tea.Batch(m.viewport.Init(), m.spinner.Tick)
48−}
49−
50−func (m *messagesCmp) preloadSessions() tea.Cmd {
51− return func() tea.Msg {
52− m.mutex.Lock()
53− defer m.mutex.Unlock()
54− sessions, err := m.app.Sessions.List(context.Background())
55− if err != nil {
56− return util.ReportError(err)()
57− }
58− if len(sessions) == 0 {
59− return nil
60− }
61− if len(sessions) > 20 {
62− sessions = sessions[:20]
63− }
64− for _, s := range sessions {
65− messages, err := m.app.Messages.List(context.Background(), s.ID)
66− if err != nil {
67− return util.ReportError(err)()
68− }
69− if len(messages) == 0 {
70− continue
71− }
72− m.cacheSessionMessages(messages, m.width)
73−
74− }
75− logging.Debug("preloaded sessions")
76−
77− return func() tea.Msg {
78− return renderFinishedMsg{}
79− }
80− }
81−}
82−
83−func (m *messagesCmp) cacheSessionMessages(messages []message.Message, width int) {
84− pos := 0
85− if m.width == 0 {
86− return
87− }
88− for inx, msg := range messages {
89− switch msg.Role {
90− case message.User:
91− userMsg := renderUserMessage(
92− msg,
93− false,
94− width,
95− pos,
96− )
97− m.cachedContent[msg.ID] = cacheItem{
98− width: width,
99− content: []uiMessage{userMsg},
100− }
101− pos += userMsg.height + 1 // + 1 for spacing
102− case message.Assistant:
103− assistantMessages := renderAssistantMessage(
104− msg,
105− inx,
106− messages,
107− m.app.Messages,
108− "",
109− width,
110− pos,
111− )
112− for _, msg := range assistantMessages {
113− pos += msg.height + 1 // + 1 for spacing
114− }
115− m.cachedContent[msg.ID] = cacheItem{
116− width: width,
117− content: assistantMessages,
118− }
119− }
120− }
121}
43}
122
44
123func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
45func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -360,21 +282,35 @@ func hasToolsWithoutResponse(messages []message.Message) bool {
360 break
282 break
361 }
283 }
362 }
284 }
363− if !found {
285+ if !found && v.Finished {
364 return true
286 return true
365 }
287 }
366 }
288 }
289+ return false
290+}
367
291
292+func hasUnfinishedToolCalls(messages []message.Message) bool {
293+ toolCalls := make([]message.ToolCall, 0)
294+ for _, m := range messages {
295+ toolCalls = append(toolCalls, m.ToolCalls()...)
296+ }
297+ for _, v := range toolCalls {
298+ if !v.Finished {
299+ return true
300+ }
301+ }
368 return false
302 return false
369}
303}
370
304
371func (m *messagesCmp) working() string {
305func (m *messagesCmp) working() string {
372 text := ""
306 text := ""
373− if m.IsAgentWorking() {
307+ if m.IsAgentWorking() && len(m.messages) > 0 {
374 task := "Thinking..."
308 task := "Thinking..."
375 lastMessage := m.messages[len(m.messages)-1]
309 lastMessage := m.messages[len(m.messages)-1]
376 if hasToolsWithoutResponse(m.messages) {
310 if hasToolsWithoutResponse(m.messages) {
377 task = "Waiting for tool response..."
311 task = "Waiting for tool response..."
312+ } else if hasUnfinishedToolCalls(m.messages) {
313+ task = "Building tool call..."
378 } else if !lastMessage.IsFinished() {
314 } else if !lastMessage.IsFinished() {
379 task = "Generating..."
315 task = "Generating..."
380 }
316 }
@@ -434,8 +370,7 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
434 delete(m.cachedContent, msg.ID)
370 delete(m.cachedContent, msg.ID)
435 }
371 }
436 m.uiMessages = make([]uiMessage, 0)
372 m.uiMessages = make([]uiMessage, 0)
437− m.renderView()
373+ return nil
438− return m.preloadSessions()
439}
374}
440
375
441func (m *messagesCmp) GetSize() (int, int) {
376func (m *messagesCmp) GetSize() (int, int) {
@@ -446,16 +381,16 @@ func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
446 if m.session.ID == session.ID {
381 if m.session.ID == session.ID {
447 return nil
382 return nil
448 }
383 }
384+ m.session = session
385+ messages, err := m.app.Messages.List(context.Background(), session.ID)
386+ if err != nil {
387+ return util.ReportError(err)
388+ }
389+ m.messages = messages
390+ m.currentMsgID = m.messages[len(m.messages)-1].ID
391+ delete(m.cachedContent, m.currentMsgID)
449 m.rendering = true
392 m.rendering = true
450 return func() tea.Msg {
393 return func() tea.Msg {
451− m.session = session
452− messages, err := m.app.Messages.List(context.Background(), session.ID)
453− if err != nil {
454− return util.ReportError(err)
455− }
456− m.messages = messages
457− m.currentMsgID = m.messages[len(m.messages)-1].ID
458− delete(m.cachedContent, m.currentMsgID)
459 m.renderView()
394 m.renderView()
460 return renderFinishedMsg{}
395 return renderFinishedMsg{}
461 }
396 }
internal/tui/components/chat/message.go+75−17
@@ -113,18 +113,10 @@ func renderAssistantMessage(
113 width int,
113 width int,
114 position int,
114 position int,
115) []uiMessage {
115) []uiMessage {
116− // find the user message that is before this assistant message
117− var userMsg message.Message
118− for i := msgIndex - 1; i >= 0; i-- {
119− msg := allMessages[i]
120− if msg.Role == message.User {
121− userMsg = allMessages[i]
122− break
123− }
124− }
125−
126 messages := []uiMessage{}
116 messages := []uiMessage{}
127 content := msg.Content().String()
117 content := msg.Content().String()
118+ thinking := msg.IsThinking()
119+ thinkingContent := msg.ReasoningContent().Thinking
128 finished := msg.IsFinished()
120 finished := msg.IsFinished()
129 finishData := msg.FinishPart()
121 finishData := msg.FinishPart()
130 info := []string{}
122 info := []string{}
@@ -133,7 +125,7 @@ func renderAssistantMessage(
133 if finished {
125 if finished {
134 switch finishData.Reason {
126 switch finishData.Reason {
135 case message.FinishReasonEndTurn:
127 case message.FinishReasonEndTurn:
136− took := formatTimeDifference(userMsg.CreatedAt, finishData.Time)
128+ took := formatTimeDifference(msg.CreatedAt, finishData.Time)
137 info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
129 info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
138 fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took),
130 fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took),
139 ))
131 ))
@@ -166,6 +158,9 @@ func renderAssistantMessage(
166 })
158 })
167 position += messages[0].height
159 position += messages[0].height
168 position++ // for the space
160 position++ // for the space
161+ } else if thinking && thinkingContent != "" {
162+ // Render the thinking content
163+ content = renderMessage(thinkingContent, false, msg.ID == focusedUIMessageId, width)
169 }
164 }
170
165
171 for i, toolCall := range msg.ToolCalls() {
166 for i, toolCall := range msg.ToolCalls() {
@@ -218,10 +213,40 @@ func toolName(name string) string {
218 return "View"
213 return "View"
219 case tools.WriteToolName:
214 case tools.WriteToolName:
220 return "Write"
215 return "Write"
216+ case tools.PatchToolName:
217+ return "Patch"
221 }
218 }
222 return name
219 return name
223}
220}
224
221
222+func getToolAction(name string) string {
223+ switch name {
224+ case agent.AgentToolName:
225+ return "Preparing prompt..."
226+ case tools.BashToolName:
227+ return "Building command..."
228+ case tools.EditToolName:
229+ return "Preparing edit..."
230+ case tools.FetchToolName:
231+ return "Writing fetch..."
232+ case tools.GlobToolName:
233+ return "Finding files..."
234+ case tools.GrepToolName:
235+ return "Searching content..."
236+ case tools.LSToolName:
237+ return "Listing directory..."
238+ case tools.SourcegraphToolName:
239+ return "Searching code..."
240+ case tools.ViewToolName:
241+ return "Reading file..."
242+ case tools.WriteToolName:
243+ return "Preparing write..."
244+ case tools.PatchToolName:
245+ return "Preparing patch..."
246+ }
247+ return "Working..."
248+}
249+
225// renders params, params[0] (params[1]=params[2] ....)
250// renders params, params[0] (params[1]=params[2] ....)
226func renderParams(paramsWidth int, params ...string) string {
251func renderParams(paramsWidth int, params ...string) string {
227 if len(params) == 0 {
252 if len(params) == 0 {
@@ -490,8 +515,47 @@ func renderToolMessage(
490 if nested {
515 if nested {
491 width = width - 3
516 width = width - 3
492 }
517 }
518+ style := styles.BaseStyle.
519+ Width(width - 1).
520+ BorderLeft(true).
521+ BorderStyle(lipgloss.ThickBorder()).
522+ PaddingLeft(1).
523+ BorderForeground(styles.ForgroundDim)
524+
493 response := findToolResponse(toolCall.ID, allMessages)
525 response := findToolResponse(toolCall.ID, allMessages)
494 toolName := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
526 toolName := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
527+
528+ if !toolCall.Finished {
529+ // Get a brief description of what the tool is doing
530+ toolAction := getToolAction(toolCall.Name)
531+
532+ // toolInput := strings.ReplaceAll(toolCall.Input, "\n", " ")
533+ // truncatedInput := toolInput
534+ // if len(truncatedInput) > 10 {
535+ // truncatedInput = truncatedInput[len(truncatedInput)-10:]
536+ // }
537+ //
538+ // truncatedInput = styles.BaseStyle.
539+ // Italic(true).
540+ // Width(width - 2 - lipgloss.Width(toolName)).
541+ // Background(styles.BackgroundDim).
542+ // Foreground(styles.ForgroundMid).
543+ // Render(truncatedInput)
544+
545+ progressText := styles.BaseStyle.
546+ Width(width - 2 - lipgloss.Width(toolName)).
547+ Foreground(styles.ForgroundDim).
548+ Render(fmt.Sprintf("%s", toolAction))
549+
550+ content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolName, progressText))
551+ toolMsg := uiMessage{
552+ messageType: toolMessageType,
553+ position: position,
554+ height: lipgloss.Height(content),
555+ content: content,
556+ }
557+ return toolMsg
558+ }
495 params := renderToolParams(width-2-lipgloss.Width(toolName), toolCall)
559 params := renderToolParams(width-2-lipgloss.Width(toolName), toolCall)
496 responseContent := ""
560 responseContent := ""
497 if response != nil {
561 if response != nil {
@@ -504,12 +568,6 @@ func renderToolMessage(
504 Foreground(styles.ForgroundDim).
568 Foreground(styles.ForgroundDim).
505 Render("Waiting for response...")
569 Render("Waiting for response...")
506 }
570 }
507− style := styles.BaseStyle.
508− Width(width - 1).
509− BorderLeft(true).
510− BorderStyle(lipgloss.ThickBorder()).
511− PaddingLeft(1).
512− BorderForeground(styles.ForgroundDim)
513
571
514 parts := []string{}
572 parts := []string{}
515 if !nested {
573 if !nested {
internal/tui/layout/split.go+28
@@ -14,6 +14,10 @@ type SplitPaneLayout interface {
14 SetLeftPanel(panel Container) tea.Cmd
14 SetLeftPanel(panel Container) tea.Cmd
15 SetRightPanel(panel Container) tea.Cmd
15 SetRightPanel(panel Container) tea.Cmd
16 SetBottomPanel(panel Container) tea.Cmd
16 SetBottomPanel(panel Container) tea.Cmd
17+
18+ ClearLeftPanel() tea.Cmd
19+ ClearRightPanel() tea.Cmd
20+ ClearBottomPanel() tea.Cmd
17}
21}
18
22
19type splitPaneLayout struct {
23type splitPaneLayout struct {
@@ -192,6 +196,30 @@ func (s *splitPaneLayout) SetBottomPanel(panel Container) tea.Cmd {
192 return nil
196 return nil
193}
197}
194
198
199+func (s *splitPaneLayout) ClearLeftPanel() tea.Cmd {
200+ s.leftPanel = nil
201+ if s.width > 0 && s.height > 0 {
202+ return s.SetSize(s.width, s.height)
203+ }
204+ return nil
205+}
206+
207+func (s *splitPaneLayout) ClearRightPanel() tea.Cmd {
208+ s.rightPanel = nil
209+ if s.width > 0 && s.height > 0 {
210+ return s.SetSize(s.width, s.height)
211+ }
212+ return nil
213+}
214+
215+func (s *splitPaneLayout) ClearBottomPanel() tea.Cmd {
216+ s.bottomPanel = nil
217+ if s.width > 0 && s.height > 0 {
218+ return s.SetSize(s.width, s.height)
219+ }
220+ return nil
221+}
222+
195func (s *splitPaneLayout) BindingKeys() []key.Binding {
223func (s *splitPaneLayout) BindingKeys() []key.Binding {
196 keys := []key.Binding{}
224 keys := []key.Binding{}
197 if s.leftPanel != nil {
225 if s.leftPanel != nil {
internal/tui/page/chat.go+9−1
@@ -57,6 +57,14 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
57 if cmd != nil {
57 if cmd != nil {
58 return p, cmd
58 return p, cmd
59 }
59 }
60+ case chat.SessionSelectedMsg:
61+ if p.session.ID == "" {
62+ cmd := p.setSidebar()
63+ if cmd != nil {
64+ cmds = append(cmds, cmd)
65+ }
66+ }
67+ p.session = msg
60 case chat.EditorFocusMsg:
68 case chat.EditorFocusMsg:
61 p.editingMode = bool(msg)
69 p.editingMode = bool(msg)
62 case tea.KeyMsg:
70 case tea.KeyMsg:
@@ -91,7 +99,7 @@ func (p *chatPage) setSidebar() tea.Cmd {
91}
99}
92
100
93func (p *chatPage) clearSidebar() tea.Cmd {
101func (p *chatPage) clearSidebar() tea.Cmd {
94− return p.layout.SetRightPanel(nil)
102+ return p.layout.ClearRightPanel()
95}
103}
96
104
97func (p *chatPage) sendMessage(text string) tea.Cmd {
105func (p *chatPage) sendMessage(text string) tea.Cmd {