summaryrefslogtreecommitdiffhomepage
path: root/packages/tui
diff options
context:
space:
mode:
authoradamdotdevin <[email protected]>2025-08-13 13:28:22 -0500
committeradamdotdevin <[email protected]>2025-08-13 13:28:22 -0500
commit1357319f6f01532a3544a23d9282f9583a422608 (patch)
tree1336779e02fea755bc45c8fc6d8f6db6ccbb9ccc /packages/tui
parente729eed34d54758d4630012bf2396c064a71522e (diff)
downloadopencode-1357319f6f01532a3544a23d9282f9583a422608.tar.gz
opencode-1357319f6f01532a3544a23d9282f9583a422608.zip
feat: bash commands
Diffstat (limited to 'packages/tui')
-rw-r--r--packages/tui/internal/app/app.go79
-rw-r--r--packages/tui/internal/components/chat/editor.go23
-rw-r--r--packages/tui/internal/tui/tui.go31
3 files changed, 119 insertions, 14 deletions
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go
index 48b7d67f9..08a79e310 100644
--- a/packages/tui/internal/app/app.go
+++ b/packages/tui/internal/app/app.go
@@ -49,6 +49,7 @@ type App struct {
InitialSession *string
compactCancel context.CancelFunc
IsLeaderSequence bool
+ IsBashMode bool
}
func (a *App) Agent() *opencode.Agent {
@@ -79,6 +80,9 @@ type AgentSelectedMsg struct {
type SessionClearedMsg struct{}
type CompactSessionMsg struct{}
type SendPrompt = Prompt
+type SendBash = struct {
+ Command string
+}
type SetEditorContentMsg struct {
Text string
}
@@ -296,23 +300,41 @@ func (a *App) CycleRecentModel() (*App, tea.Cmd) {
}
nextIndex := 0
for i, recentModel := range recentModels {
- if a.Provider != nil && a.Model != nil && recentModel.ProviderID == a.Provider.ID && recentModel.ModelID == a.Model.ID {
+ if a.Provider != nil && a.Model != nil && recentModel.ProviderID == a.Provider.ID &&
+ recentModel.ModelID == a.Model.ID {
nextIndex = (i + 1) % len(recentModels)
break
}
}
for range recentModels {
currentRecentModel := recentModels[nextIndex%len(recentModels)]
- provider, model := findModelByProviderAndModelID(a.Providers, currentRecentModel.ProviderID, currentRecentModel.ModelID)
+ provider, model := findModelByProviderAndModelID(
+ a.Providers,
+ currentRecentModel.ProviderID,
+ currentRecentModel.ModelID,
+ )
if provider != nil && model != nil {
a.Provider, a.Model = provider, model
- a.State.AgentModel[a.Agent().Name] = AgentModel{ProviderID: provider.ID, ModelID: model.ID}
- return a, tea.Sequence(a.SaveState(), toast.NewSuccessToast(fmt.Sprintf("Switched to %s (%s)", model.Name, provider.Name)))
+ a.State.AgentModel[a.Agent().Name] = AgentModel{
+ ProviderID: provider.ID,
+ ModelID: model.ID,
+ }
+ return a, tea.Sequence(
+ a.SaveState(),
+ toast.NewSuccessToast(
+ fmt.Sprintf("Switched to %s (%s)", model.Name, provider.Name),
+ ),
+ )
}
- recentModels = append(recentModels[:nextIndex%len(recentModels)], recentModels[nextIndex%len(recentModels)+1:]...)
+ recentModels = append(
+ recentModels[:nextIndex%len(recentModels)],
+ recentModels[nextIndex%len(recentModels)+1:]...)
if len(recentModels) < 2 {
a.State.RecentlyUsedModels = recentModels
- return a, tea.Sequence(a.SaveState(), toast.NewInfoToast("Not enough valid recent models to cycle"))
+ return a, tea.Sequence(
+ a.SaveState(),
+ toast.NewInfoToast("Not enough valid recent models to cycle"),
+ )
}
}
a.State.RecentlyUsedModels = recentModels
@@ -464,10 +486,19 @@ func (a *App) InitializeProvider() tea.Cmd {
// Priority 3: Current agent's preferred model
if selectedProvider == nil && a.Agent().Model.ModelID != "" {
- if provider, model := findModelByProviderAndModelID(providers, a.Agent().Model.ProviderID, a.Agent().Model.ModelID); provider != nil && model != nil {
+ if provider, model := findModelByProviderAndModelID(providers, a.Agent().Model.ProviderID, a.Agent().Model.ModelID); provider != nil &&
+ model != nil {
selectedProvider = provider
selectedModel = model
- slog.Debug("Selected model from current agent", "provider", provider.ID, "model", model.ID, "agent", a.Agent().Name)
+ slog.Debug(
+ "Selected model from current agent",
+ "provider",
+ provider.ID,
+ "model",
+ model.ID,
+ "agent",
+ a.Agent().Name,
+ )
} else {
slog.Debug("Agent model not found", "provider", a.Agent().Model.ProviderID, "model", a.Agent().Model.ModelID, "agent", a.Agent().Name)
}
@@ -724,6 +755,38 @@ func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) {
return a, tea.Batch(cmds...)
}
+func (a *App) SendBash(ctx context.Context, command string) (*App, tea.Cmd) {
+ var cmds []tea.Cmd
+ if a.Session.ID == "" {
+ session, err := a.CreateSession(ctx)
+ if err != nil {
+ return a, toast.NewErrorToast(err.Error())
+ }
+ a.Session = session
+ cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
+ }
+
+ cmds = append(cmds, func() tea.Msg {
+ _, err := a.Client.Session.Bash(
+ context.Background(),
+ a.Session.ID,
+ opencode.SessionBashParams{
+ Agent: opencode.F(a.Agent().Name),
+ Command: opencode.F(command),
+ },
+ )
+ if err != nil {
+ slog.Error("Failed to submit bash command", "error", err)
+ return toast.NewErrorToast("Failed to submit bash command")()
+ }
+ return nil
+ })
+
+ // The actual response will come through SSE
+ // For now, just return success
+ return a, tea.Batch(cmds...)
+}
+
func (a *App) Cancel(ctx context.Context, sessionID string) error {
// Cancel any running compact operation
if a.compactCancel != nil {
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go
index 980d5262c..a51f158ce 100644
--- a/packages/tui/internal/components/chat/editor.go
+++ b/packages/tui/internal/components/chat/editor.go
@@ -39,6 +39,7 @@ type EditorComponent interface {
Focus() (tea.Model, tea.Cmd)
Blur()
Submit() (tea.Model, tea.Cmd)
+ SubmitBash() (tea.Model, tea.Cmd)
Clear() (tea.Model, tea.Cmd)
Paste() (tea.Model, tea.Cmd)
Newline() (tea.Model, tea.Cmd)
@@ -342,6 +343,14 @@ func (m *editorComponent) Content() string {
Padding(0, 0, 0, 1).
Bold(true)
prompt := promptStyle.Render(">")
+ borderForeground := t.Border()
+ if m.app.IsLeaderSequence {
+ borderForeground = t.Accent()
+ }
+ if m.app.IsBashMode {
+ borderForeground = t.Secondary()
+ prompt = promptStyle.Render("!")
+ }
m.textarea.SetWidth(width - 6)
textarea := lipgloss.JoinHorizontal(
@@ -349,10 +358,6 @@ func (m *editorComponent) Content() string {
prompt,
m.textarea.View(),
)
- borderForeground := t.Border()
- if m.app.IsLeaderSequence {
- borderForeground = t.Accent()
- }
textarea = styles.NewStyle().
Background(t.BackgroundElement()).
Width(width).
@@ -489,6 +494,16 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
+func (m *editorComponent) SubmitBash() (tea.Model, tea.Cmd) {
+ command := m.textarea.Value()
+ var cmds []tea.Cmd
+ updated, cmd := m.Clear()
+ m = updated.(*editorComponent)
+ cmds = append(cmds, cmd)
+ cmds = append(cmds, util.CmdHandler(app.SendBash{Command: command}))
+ return m, tea.Batch(cmds...)
+}
+
func (m *editorComponent) Clear() (tea.Model, tea.Cmd) {
m.textarea.Reset()
m.historyIndex = -1
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 0262f5077..f57470840 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -151,6 +151,23 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
+ if a.app.IsBashMode {
+ if keyString == "backspace" && a.editor.Length() == 0 {
+ a.app.IsBashMode = false
+ return a, nil
+ }
+
+ if keyString == "enter" || keyString == "esc" || keyString == "ctrl+c" {
+ a.app.IsBashMode = false
+ if keyString == "enter" {
+ updated, cmd := a.editor.SubmitBash()
+ a.editor = updated.(chat.EditorComponent)
+ cmds = append(cmds, cmd)
+ }
+ return a, tea.Batch(cmds...)
+ }
+ }
+
// 1. Handle active modal
if a.modal != nil {
switch keyString {
@@ -189,7 +206,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// 3. Handle completions trigger
if keyString == "/" &&
!a.showCompletionDialog &&
- a.editor.Value() == "" {
+ a.editor.Value() == "" &&
+ !a.app.IsBashMode {
a.showCompletionDialog = true
updated, cmd := a.editor.Update(msg)
@@ -207,7 +225,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Handle file completions trigger
if keyString == "@" &&
- !a.showCompletionDialog {
+ !a.showCompletionDialog &&
+ !a.app.IsBashMode {
a.showCompletionDialog = true
updated, cmd := a.editor.Update(msg)
@@ -223,6 +242,11 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, tea.Sequence(cmds...)
}
+ if keyString == "!" && a.editor.Value() == "" {
+ a.app.IsBashMode = true
+ return a, nil
+ }
+
if a.showCompletionDialog {
switch keyString {
case "tab", "enter", "esc", "ctrl+c", "up", "down", "ctrl+p", "ctrl+n":
@@ -378,6 +402,9 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.showCompletionDialog = false
a.app, cmd = a.app.SendPrompt(context.Background(), msg)
cmds = append(cmds, cmd)
+ case app.SendBash:
+ a.app, cmd = a.app.SendBash(context.Background(), msg.Command)
+ cmds = append(cmds, cmd)
case app.SetEditorContentMsg:
// Set the editor content without sending
a.editor.SetValueWithAttachments(msg.Text)