Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281package tui
import ( "context" "errors" "fmt" "slices" "strings"
"albedo/cli/internal/daemon" tea "charm.land/bubbletea/v2")
func parseShellInput(input string) (string, bool) { if !strings.HasPrefix(input, "!") { return "", false } return strings.TrimSpace(strings.TrimPrefix(input, "!")), true}
func (m *ChatModel) startShellCommand(input string, cmds *[]tea.Cmd) { command, recognized := parseShellInput(input) if !recognized { return } if m.shellSubmitting { m.TextArea.SetValue(input) m.AddError("A shell command submission is still being resolved.") return } if command == "" { m.TextArea.SetValue(input) m.AddError("Type a shell command after !.") return } if m.Host != "" { m.TextArea.SetValue(input) m.AddError("! shell commands are unavailable for remote workspaces; no local command was run.") return } for _, entry := range m.History.Entries() { if entry.Kind == EntryShell && entry.ShellCommand != nil && entry.ShellCommand.Status == "running" { m.TextArea.SetValue(input) m.AddError("A shell command is already running. Cancel it or wait for it to finish.") return } } if m.shellIDsByCommand == nil { m.shellIDsByCommand = map[string]string{} } id := m.shellIDsByCommand[command] if id == "" { var err error id, err = daemon.NewShellCommandID() if err != nil { m.TextArea.SetValue(input) m.AddError(err.Error()) return } m.shellIDsByCommand[command] = id } m.ClearNotices() m.shellSubmitting = true *cmds = append(*cmds, m.shellSubmitCmd(id, command, input)) m.AddNotice("starting shell command…") m.syncLayout()}
func (m ChatModel) shellSubmitCmd(id, command, original string) tea.Cmd { session, generation, client := m.SessionID, m.Generation, m.client return func() tea.Msg { result, err := client.PutShellCommand(context.Background(), id, command) if err != nil { // A lost response is ambiguous. Query the same id before presenting a // retry; if that too fails, the retained ID makes a later retry safe. if recovered, readErr := client.GetShellCommand(context.Background(), id); readErr == nil && recovered.Command == command { result, err = recovered, nil } } return ChatShellSubmitMsg{Err: err, Command: result, SessionID: session, OriginalInput: original, Generation: generation} }}
func shellEntry(command daemon.ShellCommand) HistoryEntry { seq := int64(0) if command.HistorySeq != nil { seq = *command.HistorySeq } return HistoryEntry{ID: command.ID, Kind: EntryShell, ShellCommand: &command, Seq: seq}}
func (m *ChatModel) upsertShellCommand(command daemon.ShellCommand) { entry := shellEntry(command) if at := m.History.Find(command.ID); at >= 0 { old := m.History.Entries()[at] if old.ShellCommand != nil { previous := old.ShellCommand if previous.Status != "running" && command.Status == "running" { command.Status = previous.Status command.ExitCode, command.Error = previous.ExitCode, previous.Error command.Output, command.OutputBytes, command.Truncated = previous.Output, previous.OutputBytes, previous.Truncated } if previous.Delivery == "delivered" && command.Delivery != "delivered" { command.Delivery = "delivered" } } entry = shellEntry(command) entry.ShellOutputLoaded = old.ShellOutputLoaded entry.ShellExpandedOutput = old.ShellExpandedOutput if entry.ShellOutputLoaded && command.OutputBytes > int64(len(entry.ShellExpandedOutput)) { entry.ShellOutputLoaded, entry.ShellExpandedOutput = false, "" } m.History.Replace(entry) } else { m.History.Append(entry) } m.burstEpoch++ // The shell card may change without changing its history ID. m.rebuildSettledLines() m.refreshViewportContent()}
func (m *ChatModel) cancelShellCommand(id string) tea.Cmd { for _, entry := range m.History.Entries() { if entry.Kind == EntryShell && entry.ShellCommand != nil && entry.ShellCommand.ID == id && entry.ShellCommand.Status == "running" { session, generation, client := m.SessionID, m.Generation, m.client return func() tea.Msg { command, err := client.StopShellCommand(context.Background(), id) return ChatShellCancelMsg{Err: err, Command: command, SessionID: session, Generation: generation} } } } return nil}
func (m *ChatModel) shellOutputCmd(id string) tea.Cmd { if m.shellOutputLoading == nil { m.shellOutputLoading = map[string]bool{} } if m.shellOutputLoading[id] { return nil } var target int64 for _, entry := range m.History.Entries() { if entry.ID == id && entry.ShellCommand != nil { target = entry.ShellCommand.OutputBytes break } } if target <= 0 { return nil } if m.Renderer.Open == nil { m.Renderer.Open = map[string]bool{} } for _, entry := range m.History.Entries() { if entry.ID != id && entry.ShellOutputLoaded { m.Renderer.Open[entryKey(entry)] = false entry.ShellOutputLoaded = false entry.ShellExpandedOutput = "" m.History.Replace(entry) } } m.shellOutputLoading[id] = true session, generation, client := m.SessionID, m.Generation, m.client return func() tea.Msg { var output strings.Builder offset := int64(0) for offset < target { page, err := client.GetShellCommandOutput(context.Background(), id, offset, 65536) if err != nil { return ChatShellOutputMsg{Err: err, ID: id, SessionID: session, Generation: generation} } output.WriteString(page.Text) offset += int64(len(page.Text)) if page.NextOffset == nil || *page.NextOffset <= offset-int64(len(page.Text)) { break } if *page.NextOffset != offset { return ChatShellOutputMsg{Err: errors.New("shell output page offset changed unexpectedly"), ID: id, SessionID: session, Generation: generation} } } if offset != target { return ChatShellOutputMsg{Err: errors.New("shell output ended before the retained cutoff"), ID: id, SessionID: session, Generation: generation} } return ChatShellOutputMsg{ID: id, Text: output.String(), SessionID: session, Generation: generation} }}
func (m *ChatModel) expandShellEntry(entry HistoryEntry) tea.Cmd { if entry.ShellCommand == nil || entry.ShellOutputLoaded { return nil } if entry.ShellCommand.OutputBytes <= int64(len(entry.ShellCommand.Output)) { entry.ShellOutputLoaded, entry.ShellExpandedOutput = true, entry.ShellCommand.Output m.History.Replace(entry) m.rebuildSettledLines() m.refreshViewportContent() return nil } return m.shellOutputCmd(entry.ShellCommand.ID)}
func (m *ChatModel) collapseShellEntry(entry HistoryEntry) { entry.ShellOutputLoaded, entry.ShellExpandedOutput = false, "" m.History.Replace(entry) m.rebuildSettledLines() m.refreshViewportContent()}
func (m *ChatModel) toggleLatestShell() tea.Cmd { entries := m.History.Entries() for _, entry := range slices.Backward(entries) { if entry.Kind != EntryShell || entry.ShellCommand == nil { continue } key := entryKey(entry) if m.Renderer.Open == nil { m.Renderer.Open = map[string]bool{} } m.Renderer.Open[key] = !m.Renderer.Open[key] if m.Renderer.Open[key] { return m.expandShellEntry(entry) } m.collapseShellEntry(entry) return nil } return nil}
func (m *ChatModel) handleShellResult(msg tea.Msg) (tea.Cmd, bool) { switch msg := msg.(type) { case ChatShellSubmitMsg: if msg.SessionID != m.SessionID || msg.Generation != m.Generation { return nil, true } m.shellSubmitting = false if msg.Err != nil { m.TextArea.SetValue(msg.OriginalInput) m.syncLayout() m.AddError(fmt.Sprintf("shell command was not confirmed: %v", msg.Err)) return nil, true } delete(m.shellIDsByCommand, msg.Command.Command) m.upsertShellCommand(msg.Command) return nil, true case ChatShellCancelMsg: if msg.SessionID != m.SessionID || msg.Generation != m.Generation { return nil, true } if msg.Err != nil { m.AddError(msg.Err.Error()) } else { m.upsertShellCommand(msg.Command) } return nil, true case ChatShellOutputMsg: if msg.SessionID != m.SessionID || msg.Generation != m.Generation { return nil, true } delete(m.shellOutputLoading, msg.ID) if msg.Err != nil { m.AddError(msg.Err.Error()) return nil, true } if at := m.History.Find(msg.ID); at >= 0 { entry := m.History.Entries()[at] if entry.ShellCommand != nil { if entry.ShellCommand.OutputBytes > int64(len(msg.Text)) { return m.shellOutputCmd(msg.ID), true } entry.ShellExpandedOutput, entry.ShellOutputLoaded = msg.Text, true m.History.Replace(entry) m.rebuildSettledLines() m.refreshViewportContent() } } return nil, true } return nil, false}