Skip to content

Commit 223dde3

Browse files
authored
Run shell commands in the middle of a thread, with the ! prefix (#75)
* Run shell commands in the middle of a thread, with the ! prefix Signed-off-by: David Gageot <david.gageot@docker.com> * Remove duplication Signed-off-by: David Gageot <david.gageot@docker.com> * Remove agent context Signed-off-by: David Gageot <david.gageot@docker.com> --------- Signed-off-by: David Gageot <david.gageot@docker.com>
1 parent f938b92 commit 223dde3

6 files changed

Lines changed: 50 additions & 38 deletions

File tree

internal/app/app.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package app
22

33
import (
44
"context"
5+
"os/exec"
6+
"strings"
57

68
tea "github.com/charmbracelet/bubbletea/v2"
79

@@ -41,6 +43,14 @@ func (a *App) Team() *team.Team {
4143
// Run one agent loop
4244
func (a *App) Run(ctx context.Context, message string) {
4345
go func() {
46+
// Special shell command
47+
if strings.HasPrefix(message, "!") {
48+
out, _ := exec.CommandContext(ctx, "/bin/sh", "-c", message[1:]).CombinedOutput()
49+
a.events <- runtime.ShellOutput("$ " + message[1:] + "\n" + string(out))
50+
return
51+
}
52+
53+
// User message
4454
a.session.AddMessage(session.UserMessage(a.agentFilename, message))
4555
for event := range a.runtime.RunStream(ctx, a.session) {
4656
a.events <- event

internal/tui/components/message/message.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ func (mv *messageModel) Render(int) string {
100100
}
101101

102102
return strings.TrimRight(rendered, "\n\r\t ")
103+
case types.MessageTypeShellOutput:
104+
if rendered, err := mv.renderer.Render(fmt.Sprintf("```console\n%s\n```", msg.Content)); err == nil {
105+
return strings.TrimRight(rendered, "\n\r\t ")
106+
}
107+
return msg.Content
103108
case types.MessageTypeSeparator:
104109
return styles.MutedStyle.Render("•" + strings.Repeat("─", mv.width-3) + "•")
105110
case types.MessageTypeError:

internal/tui/components/messages/messages.go

Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type Model interface {
3737
ClearMessages()
3838
ScrollToBottom() tea.Cmd
3939
FocusToolInConfirmation() tea.Cmd
40+
AddShellOutputMessage(content string) tea.Cmd
4041
}
4142

4243
// renderedItem represents a cached rendered message with position information
@@ -460,60 +461,38 @@ func (m *model) isAtBottom() bool {
460461

461462
// AddUserMessage adds a user message to the chat
462463
func (m *model) AddUserMessage(content string) tea.Cmd {
463-
msg := types.Message{
464+
return m.addMessage(&types.Message{
464465
Type: types.MessageTypeUser,
465466
Content: content,
466-
}
467-
468-
wasAtBottom := m.isAtBottom()
469-
m.messages = append(m.messages, msg)
470-
471-
view := m.createMessageView(&msg)
472-
m.views = append(m.views, view)
473-
474-
if wasAtBottom {
475-
return tea.Batch(view.Init(), func() tea.Msg {
476-
m.scrollToBottom()
477-
return nil
478-
})
479-
}
480-
481-
return view.Init()
467+
})
482468
}
483469

484470
func (m *model) AddErrorMessage(content string) tea.Cmd {
485-
msg := types.Message{
471+
return m.addMessage(&types.Message{
486472
Type: types.MessageTypeError,
487473
Content: content,
488-
}
489-
490-
wasAtBottom := m.isAtBottom()
491-
m.messages = append(m.messages, msg)
492-
493-
view := m.createMessageView(&msg)
494-
m.views = append(m.views, view)
474+
})
475+
}
495476

496-
if wasAtBottom {
497-
return tea.Batch(view.Init(), func() tea.Msg {
498-
m.scrollToBottom()
499-
return nil
500-
})
501-
}
502-
return view.Init()
477+
func (m *model) AddShellOutputMessage(content string) tea.Cmd {
478+
return m.addMessage(&types.Message{
479+
Type: types.MessageTypeShellOutput,
480+
Content: content,
481+
})
503482
}
504483

505484
// AddAssistantMessage adds an assistant message to the chat
506-
//
507-
//goland:noinspection ALL
508485
func (m *model) AddAssistantMessage() tea.Cmd {
509-
msg := types.Message{
486+
return m.addMessage(&types.Message{
510487
Type: types.MessageTypeAssistant,
511-
}
488+
})
489+
}
512490

491+
func (m *model) addMessage(msg *types.Message) tea.Cmd {
513492
wasAtBottom := m.isAtBottom()
514-
m.messages = append(m.messages, msg)
493+
m.messages = append(m.messages, *msg)
515494

516-
view := m.createMessageView(&msg)
495+
view := m.createMessageView(msg)
517496
m.views = append(m.views, view)
518497

519498
var cmds []tea.Cmd

internal/tui/page/chat/chat.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
192192
case *runtime.ErrorEvent:
193193
cmd := p.messages.AddErrorMessage(msg.Error)
194194
return p, tea.Batch(cmd, p.messages.ScrollToBottom())
195+
case *runtime.ShellOutputEvent:
196+
cmd := p.messages.AddShellOutputMessage(msg.Output)
197+
return p, tea.Batch(cmd, p.messages.ScrollToBottom())
195198
case *runtime.UserMessageEvent:
196199
cmd := p.messages.AddUserMessage(msg.Message)
197200
return p, tea.Batch(cmd, p.messages.ScrollToBottom())

internal/tui/types/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const (
99
MessageTypeUser MessageType = iota
1010
MessageTypeAssistant
1111
MessageTypeError
12+
MessageTypeShellOutput
1213
MessageTypeSeparator
1314
MessageTypeToolCall
1415
MessageTypeToolResult

pkg/runtime/event.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,20 @@ func Error(msg string) Event {
148148
}
149149
func (e *ErrorEvent) isEvent() {}
150150

151+
type ShellOutputEvent struct {
152+
Type string `json:"type"`
153+
Output string `json:"error"`
154+
}
155+
156+
func ShellOutput(output string) Event {
157+
return &ShellOutputEvent{
158+
Type: "shell",
159+
Output: output,
160+
}
161+
}
162+
func (e *ShellOutputEvent) isEvent() {}
163+
func (e *ShellOutputEvent) GetAgentName() string { return "" }
164+
151165
type TokenUsageEvent struct {
152166
Type string `json:"type"`
153167
Usage *Usage `json:"usage"`

0 commit comments

Comments
 (0)