on main help sign in
initial tool call stream
commit2de51274177432b559be3b7deb1f14b9539f2994 authorKujtim Hoxha <kujtimii.h@gmail.com> dateSat 19 Apr 2025, 16:35 (2 years ago) parent2b5a33e
11 files changed+261−136
internal/llm/agent/agent.go+22 internal/llm/provider/anthropic.go+46−14 internal/llm/provider/openai.go+5−4 internal/llm/provider/provider.go+5−2 internal/message/content.go+43 internal/message/message.go+2 internal/pubsub/broker.go−7 internal/tui/components/chat/list.go+26−91 internal/tui/components/chat/message.go+75−17 internal/tui/layout/split.go+28 internal/tui/page/chat.go+9−1
internal/llm/agent/agent.go+22
@@ -380,6 +380,21 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
380380 case provider.EventContentDelta:
381381 assistantMsg.AppendContent(event.Content)
382382 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)
383398 case provider.EventError:
384399 if errors.Is(event.Error, context.Canceled) {
385400 logging.InfoPersist(fmt.Sprintf("Event processing canceled for session: %s", sessionID))
@@ -456,6 +471,13 @@ func createAgentProvider(agentName config.AgentName) (provider.Provider, error)
456471 provider.WithReasoningEffort(agentConfig.ReasoningEffort),
457472 ),
458473 )
474+ } else if model.Provider == models.ProviderAnthropic && model.CanReason {
475+ opts = append(
476+ opts,
477+ provider.WithAnthropicOptions(
478+ provider.WithAnthropicShouldThinkFn(provider.DefaultShouldThinkFn),
479+ ),
480+ )
459481 }
460482 agentProvider, err := provider.NewProvider(
461483 model.Provider,
internal/llm/provider/anthropic.go+46−14
@@ -93,8 +93,7 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
9393 }
9494
9595 if len(blocks) == 0 {
96− logging.Warn("There is a message without content, investigate")
97− // This should never happend but we log this because we might have a bug in our cleanup method
96+ logging.Warn("There is a message without content, investigate, this should not happen")
9897 continue
9998 }
10099 anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...))
@@ -196,8 +195,8 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
196195 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
197196 cfg := config.Get()
198197 if cfg.Debug {
199− jsonData, _ := json.Marshal(preparedMessages)
200− logging.Debug("Prepared messages", "messages", string(jsonData))
198+ // jsonData, _ := json.Marshal(preparedMessages)
199+ // logging.Debug("Prepared messages", "messages", string(jsonData))
201200 }
202201 attempts := 0
203202 for {
@@ -243,8 +242,8 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
243242 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
244243 cfg := config.Get()
245244 if cfg.Debug {
246− jsonData, _ := json.Marshal(preparedMessages)
247− logging.Debug("Prepared messages", "messages", string(jsonData))
245+ // jsonData, _ := json.Marshal(preparedMessages)
246+ // logging.Debug("Prepared messages", "messages", string(jsonData))
248247 }
249248 attempts := 0
250249 eventChan := make(chan ProviderEvent)
@@ -257,6 +256,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
257256 )
258257 accumulatedMessage := anthropic.Message{}
259258
259+ currentToolCallID := ""
260260 for anthropicStream.Next() {
261261 event := anthropicStream.Current()
262262 err := accumulatedMessage.Accumulate(event)
@@ -267,7 +267,19 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
267267
268268 switch event := event.AsAny().(type) {
269269 case anthropic.ContentBlockStartEvent:
270− eventChan <- ProviderEvent{Type: EventContentStart}
270+ if event.ContentBlock.Type == "text" {
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+ }
271283
272284 case anthropic.ContentBlockDeltaEvent:
273285 if event.Delta.Type == "thinking_delta" && event.Delta.Thinking != "" {
@@ -280,11 +292,30 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
280292 Type: EventContentDelta,
281293 Content: event.Delta.Text,
282294 }
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+ }
283306 }
284− // TODO: check if we can somehow stream tool calls
285−
286307 case anthropic.ContentBlockStopEvent:
287− eventChan <- ProviderEvent{Type: EventContentStop}
308+ if currentToolCallID != "" {
309+ eventChan <- ProviderEvent{
310+ Type: EventToolUseStop,
311+ ToolCall: &message.ToolCall{
312+ ID: currentToolCallID,
313+ },
314+ }
315+ currentToolCallID = ""
316+ } else {
317+ eventChan <- ProviderEvent{Type: EventContentStop}
318+ }
288319
289320 case anthropic.MessageStopEvent:
290321 content := ""
@@ -378,10 +409,11 @@ func (a *anthropicClient) toolCalls(msg anthropic.Message) []message.ToolCall {
378409 switch variant := block.AsAny().(type) {
379410 case anthropic.ToolUseBlock:
380411 toolCall := message.ToolCall{
381− ID: variant.ID,
382− Name: variant.Name,
383− Input: string(variant.Input),
384− Type: string(variant.Type),
412+ ID: variant.ID,
413+ Name: variant.Name,
414+ Input: string(variant.Input),
415+ Type: string(variant.Type),
416+ Finished: true,
385417 }
386418 toolCalls = append(toolCalls, toolCall)
387419 }
internal/llm/provider/openai.go+5−4
@@ -344,10 +344,11 @@ func (o *openaiClient) toolCalls(completion openai.ChatCompletion) []message.Too
344344 if len(completion.Choices) > 0 && len(completion.Choices[0].Message.ToolCalls) > 0 {
345345 for _, call := range completion.Choices[0].Message.ToolCalls {
346346 toolCall := message.ToolCall{
347− ID: call.ID,
348− Name: call.Function.Name,
349− Input: call.Function.Arguments,
350− Type: "function",
347+ ID: call.ID,
348+ Name: call.Function.Name,
349+ Input: call.Function.Arguments,
350+ Type: "function",
351+ Finished: true,
351352 }
352353 toolCalls = append(toolCalls, toolCall)
353354 }
internal/llm/provider/provider.go+5−2
@@ -15,6 +15,9 @@ const maxRetries = 8
1515
1616const (
1717 EventContentStart EventType = "content_start"
18+ EventToolUseStart EventType = "tool_use_start"
19+ EventToolUseDelta EventType = "tool_use_delta"
20+ EventToolUseStop EventType = "tool_use_stop"
1821 EventContentDelta EventType = "content_delta"
1922 EventThinkingDelta EventType = "thinking_delta"
2023 EventContentStop EventType = "content_stop"
@@ -43,8 +46,8 @@ type ProviderEvent struct {
4346 Content string
4447 Thinking string
4548 Response *ProviderResponse
46−
47− Error error
49+ ToolCall *message.ToolCall
50+ Error error
4851}
4952type Provider interface {
5053 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) {
233233 }
234234}
235235
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+
236270func (m *Message) AddToolCall(tc ToolCall) {
237271 for i, part := range m.Parts {
238272 if c, ok := part.(ToolCall); ok {
@@ -246,6 +280,15 @@ func (m *Message) AddToolCall(tc ToolCall) {
246280}
247281
248282func (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
249292 for _, toolCall := range tc {
250293 m.Parts = append(m.Parts, toolCall)
251294 }
internal/message/message.go+2
@@ -5,6 +5,7 @@ import (
55 "database/sql"
66 "encoding/json"
77 "fmt"
8+ "time"
89
910 "github.com/google/uuid"
1011 "github.com/kujtimiihoxha/opencode/internal/db"
@@ -116,6 +117,7 @@ func (s *service) Update(ctx context.Context, message Message) error {
116117 if err != nil {
117118 return err
118119 }
120+ message.UpdatedAt = time.Now().Unix()
119121 s.Publish(pubsub.UpdatedEvent, message)
120122 return nil
121123}
internal/pubsub/broker.go−7
@@ -7,13 +7,6 @@ import (
77
88const bufferSize = 1024
99
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−
1710// Broker allows clients to publish events and subscribe to events
1811type Broker[T any] struct {
1912 subs map[chan Event[T]]struct{} // subscriptions
internal/tui/components/chat/list.go+26−91
@@ -4,8 +4,6 @@ import (
44 "context"
55 "fmt"
66 "math"
7− "sync"
8− "time"
97
108 "github.com/charmbracelet/bubbles/key"
119 "github.com/charmbracelet/bubbles/spinner"
@@ -13,7 +11,6 @@ import (
1311 tea "github.com/charmbracelet/bubbletea"
1412 "github.com/charmbracelet/lipgloss"
1513 "github.com/kujtimiihoxha/opencode/internal/app"
16− "github.com/kujtimiihoxha/opencode/internal/logging"
1714 "github.com/kujtimiihoxha/opencode/internal/message"
1815 "github.com/kujtimiihoxha/opencode/internal/pubsub"
1916 "github.com/kujtimiihoxha/opencode/internal/session"
@@ -35,89 +32,14 @@ type messagesCmp struct {
3532 messages []message.Message
3633 uiMessages []uiMessage
3734 currentMsgID string
38− mutex sync.Mutex
3935 cachedContent map[string]cacheItem
4036 spinner spinner.Model
41− lastUpdate time.Time
4237 rendering bool
4338}
4439type renderFinishedMsg struct{}
4540
4641func (m *messagesCmp) Init() tea.Cmd {
47− return tea.Batch(m.viewport.Init())
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− }
42+ return tea.Batch(m.viewport.Init(), m.spinner.Tick)
12143}
12244
12345func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -360,21 +282,35 @@ func hasToolsWithoutResponse(messages []message.Message) bool {
360282 break
361283 }
362284 }
363− if !found {
285+ if !found && v.Finished {
364286 return true
365287 }
366288 }
289+ return false
290+}
367291
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+ }
368302 return false
369303}
370304
371305func (m *messagesCmp) working() string {
372306 text := ""
373− if m.IsAgentWorking() {
307+ if m.IsAgentWorking() && len(m.messages) > 0 {
374308 task := "Thinking..."
375309 lastMessage := m.messages[len(m.messages)-1]
376310 if hasToolsWithoutResponse(m.messages) {
377311 task = "Waiting for tool response..."
312+ } else if hasUnfinishedToolCalls(m.messages) {
313+ task = "Building tool call..."
378314 } else if !lastMessage.IsFinished() {
379315 task = "Generating..."
380316 }
@@ -434,8 +370,7 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
434370 delete(m.cachedContent, msg.ID)
435371 }
436372 m.uiMessages = make([]uiMessage, 0)
437− m.renderView()
438− return m.preloadSessions()
373+ return nil
439374}
440375
441376func (m *messagesCmp) GetSize() (int, int) {
@@ -446,16 +381,16 @@ func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
446381 if m.session.ID == session.ID {
447382 return nil
448383 }
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)
449392 m.rendering = true
450393 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)
459394 m.renderView()
460395 return renderFinishedMsg{}
461396 }
internal/tui/components/chat/message.go+75−17
@@ -113,18 +113,10 @@ func renderAssistantMessage(
113113 width int,
114114 position int,
115115) []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−
126116 messages := []uiMessage{}
127117 content := msg.Content().String()
118+ thinking := msg.IsThinking()
119+ thinkingContent := msg.ReasoningContent().Thinking
128120 finished := msg.IsFinished()
129121 finishData := msg.FinishPart()
130122 info := []string{}
@@ -133,7 +125,7 @@ func renderAssistantMessage(
133125 if finished {
134126 switch finishData.Reason {
135127 case message.FinishReasonEndTurn:
136− took := formatTimeDifference(userMsg.CreatedAt, finishData.Time)
128+ took := formatTimeDifference(msg.CreatedAt, finishData.Time)
137129 info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
138130 fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took),
139131 ))
@@ -166,6 +158,9 @@ func renderAssistantMessage(
166158 })
167159 position += messages[0].height
168160 position++ // for the space
161+ } else if thinking && thinkingContent != "" {
162+ // Render the thinking content
163+ content = renderMessage(thinkingContent, false, msg.ID == focusedUIMessageId, width)
169164 }
170165
171166 for i, toolCall := range msg.ToolCalls() {
@@ -218,10 +213,40 @@ func toolName(name string) string {
218213 return "View"
219214 case tools.WriteToolName:
220215 return "Write"
216+ case tools.PatchToolName:
217+ return "Patch"
221218 }
222219 return name
223220}
224221
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+
225250// renders params, params[0] (params[1]=params[2] ....)
226251func renderParams(paramsWidth int, params ...string) string {
227252 if len(params) == 0 {
@@ -490,8 +515,47 @@ func renderToolMessage(
490515 if nested {
491516 width = width - 3
492517 }
518+ style := styles.BaseStyle.
519+ Width(width - 1).
520+ BorderLeft(true).
521+ BorderStyle(lipgloss.ThickBorder()).
522+ PaddingLeft(1).
523+ BorderForeground(styles.ForgroundDim)
524+
493525 response := findToolResponse(toolCall.ID, allMessages)
494526 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+ }
495559 params := renderToolParams(width-2-lipgloss.Width(toolName), toolCall)
496560 responseContent := ""
497561 if response != nil {
@@ -504,12 +568,6 @@ func renderToolMessage(
504568 Foreground(styles.ForgroundDim).
505569 Render("Waiting for response...")
506570 }
507− style := styles.BaseStyle.
508− Width(width - 1).
509− BorderLeft(true).
510− BorderStyle(lipgloss.ThickBorder()).
511− PaddingLeft(1).
512− BorderForeground(styles.ForgroundDim)
513571
514572 parts := []string{}
515573 if !nested {
internal/tui/layout/split.go+28
@@ -14,6 +14,10 @@ type SplitPaneLayout interface {
1414 SetLeftPanel(panel Container) tea.Cmd
1515 SetRightPanel(panel Container) tea.Cmd
1616 SetBottomPanel(panel Container) tea.Cmd
17+
18+ ClearLeftPanel() tea.Cmd
19+ ClearRightPanel() tea.Cmd
20+ ClearBottomPanel() tea.Cmd
1721}
1822
1923type splitPaneLayout struct {
@@ -192,6 +196,30 @@ func (s *splitPaneLayout) SetBottomPanel(panel Container) tea.Cmd {
192196 return nil
193197}
194198
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+
195223func (s *splitPaneLayout) BindingKeys() []key.Binding {
196224 keys := []key.Binding{}
197225 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) {
5757 if cmd != nil {
5858 return p, cmd
5959 }
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
6068 case chat.EditorFocusMsg:
6169 p.editingMode = bool(msg)
6270 case tea.KeyMsg:
@@ -91,7 +99,7 @@ func (p *chatPage) setSidebar() tea.Cmd {
9199}
92100
93101func (p *chatPage) clearSidebar() tea.Cmd {
94− return p.layout.SetRightPanel(nil)
102+ return p.layout.ClearRightPanel()
95103}
96104
97105func (p *chatPage) sendMessage(text string) tea.Cmd {