fix(cli): resolve PR review feedback and improve command execution
This commit is contained in:
14 files changed
+149
-42
No files matched your search
@@ -100,9 +100,9 @@ func (m Model) instanceTarget(row instanceRow) target {
|
||||
return result
|
||||
}
|
||||
|
||||
// primaryAction is Enter: stop what is running, start what is not. Starting
|
||||
// prefers a local image built from the exact head commit, falls back to the
|
||||
// registry image, and reports honestly when neither exists yet.
|
||||
// primaryAction is Enter: stop what is running, start what is not. A matching
|
||||
// local build and a published registry image open a small source selector;
|
||||
// otherwise the only available source is started directly.
|
||||
func (m Model) primaryAction() (Model, tea.Cmd) {
|
||||
if m.screen == screenInstances {
|
||||
row, found := m.selectedInstance()
|
||||
@@ -126,6 +126,9 @@ func (m Model) primaryAction() (Model, tea.Cmd) {
|
||||
return m.startLocal(row)
|
||||
}
|
||||
if row.hasCurrentLocalImage() {
|
||||
if m.remoteImageAvailable(row) {
|
||||
return m.selectImage(row)
|
||||
}
|
||||
return m.startLocalPR(row)
|
||||
}
|
||||
switch row.image {
|
||||
@@ -139,6 +142,18 @@ func (m Model) primaryAction() (Model, tea.Cmd) {
|
||||
return m.deployRemotePR(row)
|
||||
}
|
||||
|
||||
func (m Model) remoteImageAvailable(row devRow) bool {
|
||||
return row.kind == rowRemote && m.tagsKnown && registry.HasPRImage(m.tags, row.pr.Number)
|
||||
}
|
||||
|
||||
func (m Model) selectImage(row devRow) (Model, tea.Cmd) {
|
||||
m.imageSelection = imageSelection{rowKey: row.key(), choice: imageChoiceLocal}
|
||||
m.mode = modeImageSelect
|
||||
m.status, m.statusLevel = "choose an image source for "+row.label(), levelInfo
|
||||
m.relayout()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) startLocal(row devRow) (Model, tea.Cmd) {
|
||||
plan, err := run.BuildPlan(run.Options{Context: context.Background(), Tag: row.local.Tag, Demo: m.demo})
|
||||
if err != nil {
|
||||
@@ -264,9 +279,9 @@ func (m Model) focusOn(id int, title string) (Model, tea.Cmd) {
|
||||
m.sidebar.follow = true
|
||||
m.status, m.statusLevel = title+" started in the background", levelInfo
|
||||
m.relayout()
|
||||
m.refreshSidebar()
|
||||
cmd := m.refreshSidebar()
|
||||
clocks := m.animate()
|
||||
return m, clocks
|
||||
return m, tea.Batch(cmd, clocks)
|
||||
}
|
||||
|
||||
func (m Model) stopContainer(name string) (Model, tea.Cmd) {
|
||||
|
||||
@@ -19,6 +19,8 @@ func (m Model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
return m.handleFilterKey(msg)
|
||||
case modeConfirm:
|
||||
return m.handleConfirmKey(msg)
|
||||
case modeImageSelect:
|
||||
return m.handleImageSelectKey(msg)
|
||||
case modeManage:
|
||||
return m.handleManageKey(msg)
|
||||
case modeHelp:
|
||||
@@ -30,6 +32,42 @@ func (m Model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) handleImageSelectKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
switch {
|
||||
case key.Matches(msg, m.keys.Cancel):
|
||||
m.mode = modeNormal
|
||||
m.imageSelection = imageSelection{}
|
||||
m.status, m.statusLevel = "launch canceled", levelInfo
|
||||
m.relayout()
|
||||
return m, nil
|
||||
|
||||
case key.Matches(msg, m.keys.Up), key.Matches(msg, m.keys.Down):
|
||||
if m.imageSelection.choice == imageChoiceLocal {
|
||||
m.imageSelection.choice = imageChoiceRemote
|
||||
} else {
|
||||
m.imageSelection.choice = imageChoiceLocal
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case key.Matches(msg, m.keys.Enter):
|
||||
selection := m.imageSelection
|
||||
row, found := m.selectedDev()
|
||||
m.mode = modeNormal
|
||||
m.imageSelection = imageSelection{}
|
||||
if !found || row.key() != selection.rowKey {
|
||||
m.status, m.statusLevel = "image selection expired; choose a row again", levelWarn
|
||||
m.relayout()
|
||||
return m, nil
|
||||
}
|
||||
m.relayout()
|
||||
if selection.choice == imageChoiceRemote {
|
||||
return m.deployRemotePR(row)
|
||||
}
|
||||
return m.startLocalPR(row)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) handleNormalKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
switch {
|
||||
case key.Matches(msg, m.keys.Quit):
|
||||
@@ -245,8 +283,8 @@ func (m Model) handleFilterKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
m.filter.Reset()
|
||||
m.filter.Blur()
|
||||
m.relayout()
|
||||
m.refreshSidebar()
|
||||
return m, nil
|
||||
cmd := m.refreshSidebar()
|
||||
return m, cmd
|
||||
case tea.KeyEnter:
|
||||
m.mode = modeNormal
|
||||
m.filter.Blur()
|
||||
@@ -256,8 +294,8 @@ func (m Model) handleFilterKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
var command tea.Cmd
|
||||
m.filter, command = m.filter.Update(msg)
|
||||
m.applyFilter()
|
||||
m.refreshSidebar()
|
||||
return m, command
|
||||
cmd := m.refreshSidebar()
|
||||
return m, tea.Batch(command, cmd)
|
||||
}
|
||||
|
||||
func (m Model) handleOverlayKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
|
||||
@@ -38,10 +38,23 @@ const (
|
||||
modeFilter
|
||||
modeManage
|
||||
modeConfirm
|
||||
modeImageSelect
|
||||
modeHelp
|
||||
modeTasks
|
||||
)
|
||||
|
||||
type imageChoice int
|
||||
|
||||
const (
|
||||
imageChoiceLocal imageChoice = iota
|
||||
imageChoiceRemote
|
||||
)
|
||||
|
||||
type imageSelection struct {
|
||||
rowKey string
|
||||
choice imageChoice
|
||||
}
|
||||
|
||||
type statusLevel int
|
||||
|
||||
const (
|
||||
@@ -96,7 +109,8 @@ type Model struct {
|
||||
taskCursor int
|
||||
focusTask int
|
||||
|
||||
confirm confirmation
|
||||
confirm confirmation
|
||||
imageSelection imageSelection
|
||||
|
||||
includeBots bool
|
||||
demo bool
|
||||
@@ -243,8 +257,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m.applyTasks()
|
||||
|
||||
case logsChangedMsg:
|
||||
m.refreshSidebar()
|
||||
return m, waitForLogs(m.logs)
|
||||
cmd := m.refreshSidebar()
|
||||
return m, tea.Batch(cmd, waitForLogs(m.logs))
|
||||
|
||||
case ciChecksMsg:
|
||||
m.ciChecks[msg.pr] = ciCheckState{
|
||||
@@ -253,16 +267,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
err: msg.err,
|
||||
updated: time.Now(),
|
||||
}
|
||||
m.refreshSidebar()
|
||||
cmd := m.refreshSidebar()
|
||||
for _, c := range msg.checks {
|
||||
if c.IsPending() {
|
||||
pollCmd := tea.Tick(6*time.Second, func(t time.Time) tea.Msg {
|
||||
return pollCIChecksMsg{pr: msg.pr}
|
||||
})
|
||||
return m, pollCmd
|
||||
return m, tea.Batch(cmd, pollCmd)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
return m, cmd
|
||||
|
||||
case pollCIChecksMsg:
|
||||
if m.sidebar.source == sourceCI && m.selectedPRNumber() == msg.pr {
|
||||
@@ -274,12 +288,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Batch(tickRefresh(), loadContainers(m.generation))
|
||||
|
||||
case frameMsg:
|
||||
m.refreshSidebar()
|
||||
cmd := m.refreshSidebar()
|
||||
if !m.busy() {
|
||||
m.animating = false
|
||||
return m, nil
|
||||
return m, cmd
|
||||
}
|
||||
return m, tickFrame()
|
||||
return m, tea.Batch(cmd, tickFrame())
|
||||
|
||||
case spinner.TickMsg:
|
||||
if !m.busy() {
|
||||
@@ -337,8 +351,8 @@ func (m Model) applyLoaded(msg loadedMsg) (Model, tea.Cmd) {
|
||||
m.status, m.statusLevel = m.summary(), levelInfo
|
||||
}
|
||||
m.relayout()
|
||||
m.refreshSidebar()
|
||||
return m, m.syncLogStreams()
|
||||
cmd := m.refreshSidebar()
|
||||
return m, tea.Batch(cmd, m.syncLogStreams())
|
||||
}
|
||||
|
||||
func (m Model) summary() string {
|
||||
@@ -408,7 +422,9 @@ func (m Model) applyTasks() (Model, tea.Cmd) {
|
||||
commands = append(commands, clocks)
|
||||
}
|
||||
m.relayout()
|
||||
m.refreshSidebar()
|
||||
if cmd := m.refreshSidebar(); cmd != nil {
|
||||
commands = append(commands, cmd)
|
||||
}
|
||||
return m, tea.Batch(commands...)
|
||||
}
|
||||
|
||||
@@ -592,6 +608,8 @@ func (m *Model) relayout() {
|
||||
reserved += 2
|
||||
case modeConfirm:
|
||||
reserved += 3
|
||||
case modeImageSelect:
|
||||
reserved += 5
|
||||
}
|
||||
if m.status != "" {
|
||||
reserved += 2
|
||||
|
||||
@@ -104,7 +104,7 @@ func (r devRow) searchTerms() string {
|
||||
|
||||
// buildDevRows merges open pull requests with local images into a single list.
|
||||
// A local image folds into its pull request only when the revisions match, so
|
||||
// the row that Enter starts is always the freshest thing available.
|
||||
// the row exposes both sources when the registry image is available too.
|
||||
func buildDevRows(prs []gh.PR, images []docker.Image, tags map[string]bool, tagsKnown bool) []devRow {
|
||||
rows := make([]devRow, 0, len(prs)+len(images))
|
||||
indexByPR := make(map[int]int, len(prs))
|
||||
@@ -117,8 +117,8 @@ func buildDevRows(prs []gh.PR, images []docker.Image, tags map[string]bool, tags
|
||||
if matched && image.Revision != "" && image.Revision == rows[index].pr.HeadSHA {
|
||||
if rows[index].local.Tag == "" {
|
||||
rows[index].local = image
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
rows = append(rows, devRow{kind: rowLocal, local: image, image: ui.ImageLocal})
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ func (m Model) View() tea.View {
|
||||
sections = append(sections, "", m.manageBar())
|
||||
case modeConfirm:
|
||||
sections = append(sections, "", m.confirmBar())
|
||||
case modeImageSelect:
|
||||
sections = append(sections, "", m.imageSelectBar())
|
||||
}
|
||||
if m.status != "" {
|
||||
sections = append(sections, "", m.statusView())
|
||||
@@ -208,8 +210,7 @@ func (m Model) taskOverlay(width int) string {
|
||||
}
|
||||
|
||||
// detail describes the selected row in full, including the things that are too
|
||||
// wide for a table cell: branch, provenance, and the exact image that Enter
|
||||
// would run.
|
||||
// wide for a table cell: branch, provenance, and the image source Enter can run.
|
||||
func (m Model) detail() string {
|
||||
if m.screen == screenInstances {
|
||||
row, found := m.selectedInstance()
|
||||
@@ -275,6 +276,8 @@ func (m Model) detail() string {
|
||||
switch {
|
||||
case row.running:
|
||||
meta = append(meta, ui.Help.Render("enter stops it"))
|
||||
case row.hasCurrentLocalImage() && m.remoteImageAvailable(row):
|
||||
meta = append(meta, ui.Help.Render("enter chooses local or remote"))
|
||||
case row.hasCurrentLocalImage():
|
||||
meta = append(meta, ui.Local.Render("enter runs "+row.localReference()))
|
||||
case row.image == ui.ImageRemote:
|
||||
@@ -290,6 +293,29 @@ func (m Model) detail() string {
|
||||
clip(strings.Join(meta, ui.Dim.Render(" · ")), m.width)
|
||||
}
|
||||
|
||||
func (m Model) imageSelectBar() string {
|
||||
row, found := m.selectedDev()
|
||||
if !found {
|
||||
return ui.Help.Render("image selection is no longer available")
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("%s choose image for PR #%d", ui.IconDocker, row.pr.Number)
|
||||
local := m.imageChoiceLine(imageChoiceLocal, ui.IconLocal+" local", row.localReference())
|
||||
remote := m.imageChoiceLine(imageChoiceRemote, ui.IconCloud+" remote", remoteImageFor(row))
|
||||
hint := ui.Help.Render("↑/↓ choose · enter start · esc cancel")
|
||||
return strings.Join([]string{ui.Heading.Render(title), local, remote, hint}, "\n")
|
||||
}
|
||||
|
||||
func (m Model) imageChoiceLine(choice imageChoice, label, reference string) string {
|
||||
marker := " "
|
||||
style := ui.Help
|
||||
if m.imageSelection.choice == choice {
|
||||
marker = "▸ "
|
||||
style = ui.Selected
|
||||
}
|
||||
return style.Render(ui.Truncate(marker+label+" "+reference, max(m.width-2, 20)))
|
||||
}
|
||||
|
||||
// taskTray is the always-visible one-liner for the job being followed. It uses
|
||||
// the springy bubbles progress bar because a single headline bar reads better
|
||||
// when it eases towards its target.
|
||||
|
||||
Reference in new issue
Block a user