Skip to content

Commit 26ef953

Browse files
committed
ability to Ctrl+k to kills running test loop as example
1 parent 8e0e1e3 commit 26ef953

4 files changed

Lines changed: 78 additions & 13 deletions

File tree

internal/ui/aichat.go

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ type AIChatPane struct {
8383
codeBlockInfos []dirtracker.CodeBlockInfo // Code blocks with language tags
8484
blockDirMappings []string // Effective dir per code block index
8585
workspaceRoot string // From AppConfig.WorkspaceDir
86+
runningCmd *exec.Cmd // Currently running command process (for kill support)
87+
processKilled bool // Whether the process was killed by user (Ctrl+K)
8688
}
8789

8890
// AIResponseMsg is sent when AI response chunk is received.
@@ -134,6 +136,9 @@ type AIAvailabilityMsg struct {
134136
Available bool
135137
}
136138

139+
// ClearStatusMsg is sent to clear the status message in the app.
140+
type ClearStatusMsg struct{}
141+
137142
// LanguageCheckMsg is sent when a language runtime check is needed.
138143
type LanguageCheckMsg struct {
139144
FileType string // The file type being checked (e.g., "go", "python")
@@ -582,9 +587,12 @@ func (a *AIChatPane) Update(msg tea.Msg) tea.Cmd {
582587
case AINotificationMsg:
583588
a.DisplayNotification(msg.Content)
584589
case TerminalOutputMsg:
585-
a.terminalOutput = append(a.terminalOutput, msg.Line)
586-
if a.terminalMode {
587-
a.viewModeScroll = len(a.terminalOutput)
590+
// Ignore output if process was killed by user
591+
if !a.processKilled {
592+
a.terminalOutput = append(a.terminalOutput, msg.Line)
593+
if a.terminalMode {
594+
a.viewModeScroll = len(a.terminalOutput)
595+
}
588596
}
589597
return func() tea.Msg {
590598
return <-msg.Output
@@ -593,12 +601,20 @@ func (a *AIChatPane) Update(msg tea.Msg) tea.Cmd {
593601
a.cmdRunning = false
594602
a.stdinWriter = nil
595603
a.terminalInput = ""
596-
a.terminalOutput = append(a.terminalOutput, "")
597-
if msg.Err != nil {
598-
a.terminalOutput = append(a.terminalOutput, fmt.Sprintf("[Process failed: %v]", msg.Err))
599-
} else {
600-
a.terminalOutput = append(a.terminalOutput, fmt.Sprintf("[Process exited with code %d]", msg.ExitCode))
604+
605+
// Only show exit message if process wasn't killed by user
606+
if !a.processKilled {
607+
a.terminalOutput = append(a.terminalOutput, "")
608+
if msg.Err != nil {
609+
a.terminalOutput = append(a.terminalOutput, fmt.Sprintf("[Process failed: %v]", msg.Err))
610+
} else {
611+
a.terminalOutput = append(a.terminalOutput, fmt.Sprintf("[Process exited with code %d]", msg.ExitCode))
612+
}
601613
}
614+
615+
// Reset the killed flag for next execution
616+
a.processKilled = false
617+
602618
if a.terminalMode {
603619
a.viewModeScroll = len(a.terminalOutput)
604620
}
@@ -618,6 +634,9 @@ func (a *AIChatPane) Update(msg tea.Msg) tea.Cmd {
618634
func (a *AIChatPane) executeCommand(script string, cwd string) tea.Cmd {
619635
outChan := make(chan tea.Msg)
620636

637+
// Reset the killed flag for new execution
638+
a.processKilled = false
639+
621640
// Determine effective working directory
622641
effectiveDir := cwd
623642
if effectiveDir == "" {
@@ -669,13 +688,17 @@ func (a *AIChatPane) executeCommand(script string, cwd string) tea.Cmd {
669688
cmd.Dir = effectiveDir
670689
}
671690

691+
// Store the running command so it can be killed with Ctrl+K
692+
a.runningCmd = cmd
693+
672694
stdin, _ := cmd.StdinPipe()
673695
stdout, _ := cmd.StdoutPipe()
674696
stderr, _ := cmd.StderrPipe()
675697

676698
a.stdinWriter = stdin
677699

678700
if err := cmd.Start(); err != nil {
701+
a.runningCmd = nil
679702
outChan <- TerminalDoneMsg{Err: err}
680703
return
681704
}
@@ -718,6 +741,7 @@ func (a *AIChatPane) executeCommand(script string, cwd string) tea.Cmd {
718741
}
719742
}
720743
a.stdinWriter = nil
744+
a.runningCmd = nil
721745
outChan <- TerminalDoneMsg{ExitCode: exitCode, Err: err}
722746
}()
723747

@@ -858,6 +882,38 @@ func (a *AIChatPane) handleKeyPress(msg tea.KeyMsg) tea.Cmd {
858882
// When a command is running in terminal mode, forward input to the process
859883
if a.terminalMode && a.cmdRunning && a.stdinWriter != nil {
860884
switch keyStr {
885+
case "ctrl+k":
886+
// Kill the running process
887+
if a.runningCmd != nil && a.runningCmd.Process != nil {
888+
// Set flag to ignore further output
889+
a.processKilled = true
890+
// Close stdin first
891+
if a.stdinWriter != nil {
892+
a.stdinWriter.Close()
893+
}
894+
895+
// Kill the process tree (important on Windows to kill child processes)
896+
pid := a.runningCmd.Process.Pid
897+
if runtime.GOOS == "windows" {
898+
// On Windows, use taskkill to kill the entire process tree
899+
killCmd := exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprintf("%d", pid))
900+
killCmd.Run()
901+
} else {
902+
// On Unix, just kill the process
903+
a.runningCmd.Process.Kill()
904+
}
905+
906+
// Mark as not running
907+
a.cmdRunning = false
908+
a.terminalOutput = append(a.terminalOutput, "", "[Process killed by user (Ctrl+K)]")
909+
a.viewModeScroll = len(a.terminalOutput)
910+
911+
// Return a command to clear the status message in the app
912+
return func() tea.Msg {
913+
return ClearStatusMsg{}
914+
}
915+
}
916+
return nil
861917
case "esc":
862918
// Esc always exits — close stdin and let the process finish
863919
a.stdinWriter.Close()
@@ -1626,7 +1682,7 @@ func (a *AIChatPane) renderViewMode() string {
16261682
MarginBottom(1).
16271683
Padding(0, 1).
16281684
Width(a.width - 4).
1629-
Render(" ⚙ [Enter] Send Input | [Esc] Close Stdin | [↑↓/PgUp/PgDn] Scroll ")
1685+
Render(" ⚙ [Enter] Send Input | [Ctrl+K] Kill Process | [Esc] Close Stdin | [↑↓/PgUp/PgDn] Scroll ")
16301686
} else {
16311687
instructions = lipgloss.NewStyle().
16321688
Foreground(lipgloss.Color("15")).
@@ -1918,7 +1974,7 @@ func (a *AIChatPane) GetLastAssistantResponse() string {
19181974

19191975
// RunScript enters terminal mode and executes the given command, streaming
19201976
// output into the AI chat pane. Returns a tea.Cmd to start the execution.
1921-
func (a *AIChatPane) RunScript(command string, label string) tea.Cmd {
1977+
func (a *AIChatPane) RunScript(command string, label string, workingDir string) tea.Cmd {
19221978
if a.cmdRunning {
19231979
return nil
19241980
}
@@ -1937,7 +1993,7 @@ func (a *AIChatPane) RunScript(command string, label string) tea.Cmd {
19371993
}
19381994
a.viewModeScroll = 0
19391995

1940-
return a.executeCommand(command, "")
1996+
return a.executeCommand(command, workingDir)
19411997
}
19421998

19431999
// EnterConfigMode enters the configuration editor mode.

internal/ui/app.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,11 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
474474

475475
return a, nil
476476

477-
case TerminalOutputMsg, TerminalDoneMsg, AIResponseMsg, AINotificationMsg, AIAvailabilityMsg:
477+
case TerminalOutputMsg, TerminalDoneMsg, AIResponseMsg, AINotificationMsg, AIAvailabilityMsg, ClearStatusMsg:
478+
// Handle ClearStatusMsg
479+
if _, ok := msg.(ClearStatusMsg); ok {
480+
a.statusMessage = ""
481+
}
478482
cmd := a.aiPane.Update(msg)
479483
cmds = append(cmds, cmd)
480484
return a, tea.Batch(cmds...)
@@ -1154,9 +1158,10 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
11541158
a.aiPane.focused = true
11551159

11561160
fileName := filepath.Base(filePath)
1161+
fileDir := filepath.Dir(filePath)
11571162
a.statusMessage = "Running " + fileName + "..."
11581163

1159-
cmd := a.aiPane.RunScript(runCmd, fileName)
1164+
cmd := a.aiPane.RunScript(runCmd, fileName, fileDir)
11601165
cmds = append(cmds, cmd)
11611166
return a, tea.Batch(cmds...)
11621167

@@ -1751,6 +1756,8 @@ func (a *App) handleAIMessage(message string) tea.Cmd {
17511756
helpText += " Ctrl+N New file\n"
17521757
helpText += " Ctrl+S Save file\n"
17531758
helpText += " Ctrl+X Close file\n"
1759+
helpText += " Ctrl+R Run current script\n"
1760+
helpText += " Ctrl+K Kill running process (in terminal mode)\n"
17541761
helpText += " Ctrl+B Backup Picker (Restore previous versions)\n"
17551762
helpText += " Ctrl+Q Quit\n\n"
17561763
helpText += "AI\n"

internal/ui/help.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func (a *App) renderHelpDialog() string {
3838
helpText += keyStyle.Render(" Ctrl+S") + descStyle.Render(" Save file") + "\n"
3939
helpText += keyStyle.Render(" Ctrl+X") + descStyle.Render(" Close file") + "\n"
4040
helpText += keyStyle.Render(" Ctrl+R") + descStyle.Render(" Run current script") + "\n"
41+
helpText += keyStyle.Render(" Ctrl+K") + descStyle.Render(" Kill running process (in terminal mode)") + "\n"
4142
helpText += keyStyle.Render(" Ctrl+B") + descStyle.Render(" Backup Picker (Restore previous versions)") + "\n"
4243
helpText += keyStyle.Render(" Ctrl+Q") + descStyle.Render(" Quit") + "\n"
4344
helpText += "\n"

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func main() {
5454
fmt.Println(" Tab - Switch between editor and AI pane")
5555
fmt.Println(" Ctrl+S - Save current file")
5656
fmt.Println(" Ctrl+R - Execute current script")
57+
fmt.Println(" Ctrl+K - Kill running process (in terminal mode)")
5758
fmt.Println(" Ctrl+Enter - Send message to AI")
5859
fmt.Println(" Ctrl+C - Copy selected line or AI block")
5960
fmt.Println(" Ctrl+Q - Quit application")

0 commit comments

Comments
 (0)