Skip to content

Commit c1b7ae5

Browse files
authored
Merge pull request #521 from bborn/task/2097-audit-kanban-quick-input-function
Fix kanban quick input to send literal text via tmux
2 parents 9d103f0 + 318affa commit c1b7ae5

3 files changed

Lines changed: 98 additions & 1 deletion

File tree

internal/executor/executor.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,6 +1688,28 @@ func SendKeyToPane(taskID int64, keys ...string) error {
16881688
return exec.Command("tmux", args...).Run()
16891689
}
16901690

1691+
// SendLiteralTextToPane sends literal text (using tmux -l flag) followed by Enter
1692+
// to a task's executor tmux pane. Unlike SendKeyToPane, this ensures the text is
1693+
// never interpreted as a tmux key name (e.g. "Enter", "Escape", "Space").
1694+
func SendLiteralTextToPane(taskID int64, text string) error {
1695+
sessionName := TmuxSessionName(taskID)
1696+
1697+
// Check if session exists first
1698+
if err := exec.Command("tmux", "has-session", "-t", sessionName).Run(); err != nil {
1699+
return fmt.Errorf("session not found: %w", err)
1700+
}
1701+
1702+
target := sessionName + ".0"
1703+
1704+
// Send text literally (won't interpret key names)
1705+
if err := exec.Command("tmux", "send-keys", "-t", target, "-l", text).Run(); err != nil {
1706+
return err
1707+
}
1708+
1709+
// Send Enter as a key press
1710+
return exec.Command("tmux", "send-keys", "-t", target, "Enter").Run()
1711+
}
1712+
16911713
// KillAllWindowsByNameAllSessions kills ALL windows with a given name across all daemon sessions.
16921714
// Also kills any -shell variant windows.
16931715
func KillAllWindowsByNameAllSessions(windowName string) {

internal/ui/app.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4222,7 +4222,7 @@ func (m *AppModel) denyExecutorPrompt(taskID int64) tea.Cmd {
42224222
func (m *AppModel) sendTextToExecutor(taskID int64, text string) tea.Cmd {
42234223
database := m.db
42244224
return func() tea.Msg {
4225-
err := executor.SendKeyToPane(taskID, text, "Enter")
4225+
err := executor.SendLiteralTextToPane(taskID, text)
42264226
if err == nil {
42274227
database.AppendTaskLog(taskID, "user", fmt.Sprintf("Replied from kanban: %s", text))
42284228
}

internal/ui/app_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1866,6 +1866,81 @@ func TestQuickInput_EmptyEnterUnfocuses(t *testing.T) {
18661866
}
18671867
}
18681868

1869+
// TestQuickInput_EnterWithTextSendsAndUnfocuses verifies pressing Enter with text
1870+
// unfocuses quick input and produces a command (the sendTextToExecutor tea.Cmd).
1871+
func TestQuickInput_EnterWithTextSendsAndUnfocuses(t *testing.T) {
1872+
replyInput := textinput.New()
1873+
replyInput.CharLimit = 200
1874+
replyInput.Focus()
1875+
replyInput.SetValue("hello world")
1876+
1877+
kanban := NewKanbanBoard(100, 50)
1878+
task := &db.Task{ID: 42, Title: "Test task", Status: db.StatusBlocked}
1879+
kanban.SetTasks([]*db.Task{task})
1880+
kanban.SelectTask(42)
1881+
1882+
m := &AppModel{
1883+
width: 100,
1884+
height: 50,
1885+
currentView: ViewDashboard,
1886+
keys: DefaultKeyMap(),
1887+
quickInputFocused: true,
1888+
replyInput: replyInput,
1889+
tasksNeedingInput: map[int64]bool{42: true},
1890+
questionPrompts: make(map[int64]bool),
1891+
executorPrompts: make(map[int64]string),
1892+
kanban: kanban,
1893+
}
1894+
1895+
result, cmd := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
1896+
model := result.(*AppModel)
1897+
1898+
if model.quickInputFocused {
1899+
t.Error("quickInputFocused should be false after pressing Enter with text")
1900+
}
1901+
if model.replyInput.Value() != "" {
1902+
t.Error("replyInput should be cleared after sending")
1903+
}
1904+
if cmd == nil {
1905+
t.Error("expected a command to be returned for sending text to executor")
1906+
}
1907+
}
1908+
1909+
// TestQuickInput_EnterWithTextNoSelectedTask verifies pressing Enter with text
1910+
// but no selected task gracefully unfocuses without crashing.
1911+
func TestQuickInput_EnterWithTextNoSelectedTask(t *testing.T) {
1912+
replyInput := textinput.New()
1913+
replyInput.CharLimit = 200
1914+
replyInput.Focus()
1915+
replyInput.SetValue("some text")
1916+
1917+
kanban := NewKanbanBoard(100, 50)
1918+
// No tasks set - SelectedTask() returns nil
1919+
1920+
m := &AppModel{
1921+
width: 100,
1922+
height: 50,
1923+
currentView: ViewDashboard,
1924+
keys: DefaultKeyMap(),
1925+
quickInputFocused: true,
1926+
replyInput: replyInput,
1927+
tasksNeedingInput: make(map[int64]bool),
1928+
questionPrompts: make(map[int64]bool),
1929+
executorPrompts: make(map[int64]string),
1930+
kanban: kanban,
1931+
}
1932+
1933+
result, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
1934+
model := result.(*AppModel)
1935+
1936+
if model.quickInputFocused {
1937+
t.Error("quickInputFocused should be false even without a selected task")
1938+
}
1939+
if model.replyInput.Value() != "" {
1940+
t.Error("replyInput should be cleared")
1941+
}
1942+
}
1943+
18691944
// TestQuickInput_TabIgnoredWithoutBlockedTask verifies Tab does nothing
18701945
// when the selected task doesn't need input.
18711946
func TestQuickInput_TabIgnoredWithoutBlockedTask(t *testing.T) {

0 commit comments

Comments
 (0)