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
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ Any state → closed (manual)
| `c` | Close selected task |
| `d` | Delete selected task |
| `w` | Watch current execution |
| `i` | Interrupt execution |
| `/` | Filter tasks |
| `m` | Project memories |
| `s` | Settings |
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ ssh -p 2222 your-server
| `a` | Attach to tmux session |
| `o` | Open task's working directory |
| `f` | View/manage attachments |
| `i` | Interrupt execution |
| `/` | Filter tasks |
| `m` | Project memories |
| `s` | Settings |
Expand Down
60 changes: 3 additions & 57 deletions internal/ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ type KeyMap struct {
Delete key.Binding
Watch key.Binding
Attach key.Binding
Interrupt key.Binding
Filter key.Binding
Refresh key.Binding
Settings key.Binding
Expand All @@ -79,7 +78,7 @@ func (k KeyMap) FullHelp() [][]key.Binding {
{k.Left, k.Right, k.Up, k.Down},
{k.FocusBacklog, k.FocusInProgress, k.FocusBlocked, k.FocusDone},
{k.Enter, k.New, k.Queue, k.Close},
{k.Retry, k.Watch, k.Attach, k.Interrupt, k.Delete},
{k.Retry, k.Watch, k.Attach, k.Delete},
{k.Filter, k.Settings, k.Memories, k.Files},
{k.Refresh, k.Help, k.Quit},
}
Expand Down Expand Up @@ -144,10 +143,6 @@ func DefaultKeyMap() KeyMap {
key.WithKeys("a"),
key.WithHelp("a", "attach"),
),
Interrupt: key.NewBinding(
key.WithKeys("i"),
key.WithHelp("i", "interrupt"),
),
Filter: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "filter"),
Expand Down Expand Up @@ -334,8 +329,6 @@ func NewAppModel(database *db.DB, exec *executor.Executor, workingDir string) *A
func (m *AppModel) Init() tea.Cmd {
// Subscribe to real-time task events
m.eventCh = m.executor.SubscribeTaskEvents()
// Initialize interrupt key state (disabled until we know tasks are executing)
m.keys.Interrupt.SetEnabled(len(m.executor.RunningTasks()) > 0)

// Start watching database file for changes
m.startDatabaseWatcher()
Expand Down Expand Up @@ -437,9 +430,6 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.tasks = msg.tasks
m.err = msg.err

// Update interrupt key state based on whether any task is executing
m.updateInterruptKey()

// Check for newly blocked/done tasks and notify
for _, t := range m.tasks {
prevStatus := m.prevStatuses[t.ID]
Expand Down Expand Up @@ -523,7 +513,7 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.err = msg.err
}

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

case attachDoneMsg:
Expand Down Expand Up @@ -560,10 +550,7 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
m.kanban.SetTasks(m.tasks)

// Update interrupt key state based on whether any task is executing
m.updateInterruptKey()


// Update detail view if showing this task
if m.selectedTask != nil && m.selectedTask.ID == event.TaskID {
m.selectedTask = event.Task
Expand Down Expand Up @@ -893,14 +880,6 @@ func (m *AppModel) updateDashboard(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
}

case key.Matches(msg, m.keys.Interrupt):
// Interrupt the selected task if it's in progress
if task := m.kanban.SelectedTask(); task != nil {
if db.IsInProgress(task.Status) || m.executor.IsRunning(task.ID) {
return m, m.interruptTask(task.ID)
}
}

case key.Matches(msg, m.keys.Filter):
m.filtering = true
m.filterInput.Focus()
Expand Down Expand Up @@ -1021,13 +1000,6 @@ func (m *AppModel) updateDetail(msg tea.Msg) (tea.Model, tea.Cmd) {
)
}
}
if key.Matches(keyMsg, m.keys.Interrupt) && m.selectedTask != nil {
// Interrupt if tmux session exists or executor is running
sessionName := executor.TmuxSessionName(m.selectedTask.ID)
if osExec.Command("tmux", "has-session", "-t", sessionName).Run() == nil || m.executor.IsRunning(m.selectedTask.ID) {
return m, m.interruptTask(m.selectedTask.ID)
}
}
if key.Matches(keyMsg, m.keys.Delete) && m.selectedTask != nil {
// Clean up detail view first
if m.detailView != nil {
Expand Down Expand Up @@ -1505,10 +1477,6 @@ type taskRetriedMsg struct {
err error
}

type taskInterruptedMsg struct {
err error
}

type taskEventMsg struct {
event executor.TaskEvent
}
Expand Down Expand Up @@ -1737,13 +1705,6 @@ func (m *AppModel) retryTaskWithAttachments(id int64, feedback string, attachmen
}
}

func (m *AppModel) interruptTask(id int64) tea.Cmd {
return func() tea.Msg {
m.executor.Interrupt(id)
return taskInterruptedMsg{}
}
}

func (m *AppModel) waitForTaskEvent() tea.Cmd {
return func() tea.Msg {
event, ok := <-m.eventCh
Expand Down Expand Up @@ -1857,18 +1818,3 @@ func (m *AppModel) stopDatabaseWatcher() {
close(m.dbChangeCh)
}
}

// updateInterruptKey enables or disables the interrupt key based on whether any task is executing.
func (m *AppModel) updateInterruptKey() {
hasExecuting := len(m.executor.RunningTasks()) > 0
if !hasExecuting {
// Also check if any task in the list is in progress status
for _, t := range m.tasks {
if db.IsInProgress(t.Status) {
hasExecuting = true
break
}
}
}
m.keys.Interrupt.SetEnabled(hasExecuting)
}
32 changes: 9 additions & 23 deletions internal/ui/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,22 @@ package ui

import (
"testing"

"github.com/bborn/workflow/internal/db"
)

func TestInterruptKeyEnabled(t *testing.T) {
// Create a test database
database, err := db.Open(":memory:")
if err != nil {
t.Fatalf("failed to create test db: %v", err)
}
defer database.Close()

// Create app model with nil executor (we'll test the keys directly)
func TestDefaultKeyMap(t *testing.T) {
// Verify DefaultKeyMap creates valid key bindings
keys := DefaultKeyMap()

// By default, the interrupt key should be enabled
if !keys.Interrupt.Enabled() {
t.Error("interrupt key should be enabled by default")
// Check that some key bindings are properly defined
if keys.Enter.Help().Key != "enter" {
t.Error("Enter key should have help text 'enter'")
}

// After disabling, it should be disabled
keys.Interrupt.SetEnabled(false)
if keys.Interrupt.Enabled() {
t.Error("interrupt key should be disabled after SetEnabled(false)")
if keys.Quit.Help().Key != "ctrl+c" {
t.Error("Quit key should have help text 'ctrl+c'")
}

// Re-enable
keys.Interrupt.SetEnabled(true)
if !keys.Interrupt.Enabled() {
t.Error("interrupt key should be enabled after SetEnabled(true)")
if keys.New.Help().Key != "n" {
t.Error("New key should have help text 'n'")
}
}

33 changes: 12 additions & 21 deletions internal/ui/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,18 @@ import (

// WatchModel represents the task watch view.
type WatchModel struct {
database *db.DB
executor *executor.Executor
taskID int64
task *db.Task
logs []*db.TaskLog
lastLogID int64
viewport viewport.Model
spinner spinner.Model
width int
height int
ready bool
logCh chan *db.TaskLog
interrupted bool
database *db.DB
executor *executor.Executor
taskID int64
task *db.Task
logs []*db.TaskLog
lastLogID int64
viewport viewport.Model
spinner spinner.Model
width int
height int
ready bool
logCh chan *db.TaskLog
}

// NewWatchModel creates a new watch model.
Expand Down Expand Up @@ -107,13 +106,6 @@ func (m *WatchModel) Update(msg tea.Msg) (*WatchModel, tea.Cmd) {

switch msg := msg.(type) {
case tea.KeyMsg:
// Handle interrupt key
if msg.String() == "i" {
m.executor.Interrupt(m.taskID)
m.interrupted = true
return m, nil
}

var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
Expand Down Expand Up @@ -190,7 +182,6 @@ func (m *WatchModel) renderHelp() string {
desc string
}{
{"↑/↓", "scroll"},
{"i", "interrupt"},
{"q/esc", "back"},
}

Expand Down