Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions internal/ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
ViewDetail
ViewNewTask
ViewNewTaskConfirm
ViewEditTask
ViewDeleteConfirm
ViewQuitConfirm
ViewWatch
Expand All @@ -45,6 +46,7 @@ type KeyMap struct {
Enter key.Binding
Back key.Binding
New key.Binding
Edit key.Binding
Queue key.Binding
Retry key.Binding
Close key.Binding
Expand Down Expand Up @@ -115,6 +117,10 @@ func DefaultKeyMap() KeyMap {
key.WithKeys("n"),
key.WithHelp("n", "new"),
),
Edit: key.NewBinding(
key.WithKeys("e"),
key.WithHelp("e", "edit"),
),
Queue: key.NewBinding(
key.WithKeys("x"),
key.WithHelp("x", "execute"),
Expand Down Expand Up @@ -247,6 +253,10 @@ type AppModel struct {
queueConfirm *huh.Form
queueValue bool

// Edit task form state
editTaskForm *FormModel
editingTask *db.Task

// Delete confirmation state
deleteConfirm *huh.Form
deleteConfirmValue bool
Expand Down Expand Up @@ -352,6 +362,9 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.currentView == ViewNewTask && m.newTaskForm != nil {
return m.updateNewTaskForm(msg)
}
if m.currentView == ViewEditTask && m.editTaskForm != nil {
return m.updateEditTaskForm(msg)
}
if m.currentView == ViewNewTaskConfirm && m.queueConfirm != nil {
return m.updateNewTaskConfirm(msg)
}
Expand Down Expand Up @@ -511,6 +524,20 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.err = msg.err
}

case taskUpdatedMsg:
if msg.err == nil {
// Update the selected task if we're in detail view
if m.selectedTask != nil && msg.task != nil && m.selectedTask.ID == msg.task.ID {
m.selectedTask = msg.task
if m.detailView != nil {
m.detailView.UpdateTask(msg.task)
}
}
cmds = append(cmds, m.loadTasks())
} else {
m.err = msg.err
}

case taskQueuedMsg, taskClosedMsg, taskDeletedMsg, taskRetriedMsg, taskInterruptedMsg:
cmds = append(cmds, m.loadTasks())

Expand Down Expand Up @@ -618,6 +645,10 @@ func (m *AppModel) View() string {
if m.newTaskForm != nil {
return m.newTaskForm.View()
}
case ViewEditTask:
if m.editTaskForm != nil {
return m.editTaskForm.View()
}
case ViewNewTaskConfirm:
return m.viewNewTaskConfirm()
case ViewDeleteConfirm:
Expand Down Expand Up @@ -1026,6 +1057,13 @@ func (m *AppModel) updateDetail(msg tea.Msg) (tea.Model, tea.Cmd) {
m.currentView = ViewAttachments
return m, nil
}
if key.Matches(keyMsg, m.keys.Edit) && m.selectedTask != nil {
m.editingTask = m.selectedTask
m.editTaskForm = NewEditFormModel(m.db, m.selectedTask, m.width, m.height)
m.previousView = m.currentView
m.currentView = ViewEditTask
return m, m.editTaskForm.Init()
}

if m.detailView != nil {
var cmd tea.Cmd
Expand Down Expand Up @@ -1118,6 +1156,48 @@ func (m *AppModel) updateNewTaskConfirm(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd
}

func (m *AppModel) updateEditTaskForm(msg tea.Msg) (tea.Model, tea.Cmd) {
// Handle escape to cancel
if keyMsg, ok := msg.(tea.KeyMsg); ok {
if keyMsg.String() == "esc" {
m.currentView = m.previousView
m.editTaskForm = nil
m.editingTask = nil
return m, nil
}
}

// Pass all messages to the form
model, cmd := m.editTaskForm.Update(msg)
if form, ok := model.(*FormModel); ok {
m.editTaskForm = form
if form.submitted {
// Get updated task data from form
updatedTask := form.GetDBTask()
// Preserve the original task's ID and other fields
updatedTask.ID = m.editingTask.ID
updatedTask.Status = m.editingTask.Status
updatedTask.WorktreePath = m.editingTask.WorktreePath
updatedTask.BranchName = m.editingTask.BranchName
updatedTask.CreatedAt = m.editingTask.CreatedAt
updatedTask.StartedAt = m.editingTask.StartedAt
updatedTask.CompletedAt = m.editingTask.CompletedAt

m.editTaskForm = nil
m.editingTask = nil
m.currentView = m.previousView
return m, m.updateTask(updatedTask)
}
if form.cancelled {
m.currentView = m.previousView
m.editTaskForm = nil
m.editingTask = nil
return m, nil
}
}
return m, cmd
}

func (m *AppModel) showDeleteConfirm(task *db.Task) (tea.Model, tea.Cmd) {
m.pendingDeleteTask = task
m.deleteConfirmValue = false
Expand Down Expand Up @@ -1396,6 +1476,11 @@ type taskCreatedMsg struct {
err error
}

type taskUpdatedMsg struct {
task *db.Task
err error
}

type taskQueuedMsg struct {
err error
}
Expand Down Expand Up @@ -1462,6 +1547,18 @@ func (m *AppModel) createTask(t *db.Task) tea.Cmd {
}
}

func (m *AppModel) updateTask(t *db.Task) tea.Cmd {
database := m.db
exec := m.executor
return func() tea.Msg {
err := database.UpdateTask(t)
if err == nil {
exec.NotifyTaskChange("updated", t)
}
return taskUpdatedMsg{task: t, err: err}
}
}

func (m *AppModel) createTaskWithAttachments(t *db.Task, attachmentPaths []string) tea.Cmd {
exec := m.executor
database := m.db
Expand Down
1 change: 1 addition & 0 deletions internal/ui/detail.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ func (m *DetailModel) renderHelp() string {
key string
desc string
}{
{"e", "edit"},
{"r", "retry"},
{"c", "close"},
{"d", "delete"},
Expand Down
84 changes: 83 additions & 1 deletion internal/ui/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type FormModel struct {
height int
submitted bool
cancelled bool
isEdit bool // true when editing an existing task

// Current field
focused FormField
Expand All @@ -55,6 +56,83 @@ type FormModel struct {
attachments []string // Parsed file paths
}

// NewEditFormModel creates a form model pre-populated with an existing task's data for editing.
func NewEditFormModel(database *db.DB, task *db.Task, width, height int) *FormModel {
m := &FormModel{
db: database,
width: width,
height: height,
focused: FieldTitle,
types: []string{"", "code", "writing", "thinking"},
priorities: []string{"normal", "high", "low"},
priority: task.Priority,
taskType: task.Type,
project: task.Project,
isEdit: true,
}

// Set priority index
for i, p := range m.priorities {
if p == task.Priority {
m.priorityIdx = i
break
}
}

// Set type index
for i, t := range m.types {
if t == task.Type {
m.typeIdx = i
break
}
}

// Load projects
m.projects = []string{""}
if database != nil {
if projs, err := database.ListProjects(); err == nil {
for _, p := range projs {
m.projects = append(m.projects, p.Name)
}
}
}

// Set project index
for i, p := range m.projects {
if p == task.Project {
m.projectIdx = i
break
}
}

// Title input - pre-populate with existing title
m.titleInput = textinput.New()
m.titleInput.Placeholder = "What needs to be done?"
m.titleInput.Prompt = ""
m.titleInput.Focus()
m.titleInput.Width = width - 24
m.titleInput.SetValue(task.Title)

// Body textarea - pre-populate with existing body
m.bodyInput = textarea.New()
m.bodyInput.Placeholder = "Additional context (optional)"
m.bodyInput.Prompt = ""
m.bodyInput.ShowLineNumbers = false
m.bodyInput.SetWidth(width - 24)
m.bodyInput.SetHeight(4)
m.bodyInput.FocusedStyle.CursorLine = lipgloss.NewStyle()
m.bodyInput.BlurredStyle.CursorLine = lipgloss.NewStyle()
m.bodyInput.SetValue(task.Body)

// Attachments input
m.attachmentsInput = textinput.New()
m.attachmentsInput.Placeholder = "Drag files here"
m.attachmentsInput.Prompt = ""
m.attachmentsInput.Width = width - 24

return m
}

// NewFormModel creates a new form model.
func NewFormModel(database *db.DB, width, height int, workingDir string) *FormModel {
m := &FormModel{
Expand Down Expand Up @@ -347,10 +425,14 @@ func (m *FormModel) View() string {
cursorStyle := lipgloss.NewStyle().Foreground(ColorPrimary)

// Header
headerText := "New Task"
if m.isEdit {
headerText = "Edit Task"
}
header := lipgloss.NewStyle().
Bold(true).
Foreground(ColorPrimary).
Render("New Task")
Render(headerText)
b.WriteString(header)
b.WriteString("\n\n")

Expand Down