Skip to content

Commit 4e9d021

Browse files
committed
initial ability to create and test script from inside ti ide
1 parent 0bbf8bf commit 4e9d021

6 files changed

Lines changed: 172 additions & 15 deletions

File tree

internal/agentic/fixer.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,14 @@ func (f *AgenticCodeFixer) detectChangeLocations(originalLines, modifiedLines []
804804
// This is a simple heuristic that looks for common function declaration patterns
805805
// Supports bash, shell, powershell function declarations
806806
func (f *AgenticCodeFixer) detectNearbyFunction(lines []string, lineNum int) string {
807+
if len(lines) == 0 {
808+
return ""
809+
}
810+
// Clamp lineNum to valid range
811+
if lineNum >= len(lines) {
812+
lineNum = len(lines) - 1
813+
}
814+
807815
// Search backwards from the change location to find a function declaration
808816
// Look up to 20 lines back
809817
searchStart := lineNum - 20

internal/executor/executor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func (ce *CommandExecutor) ExecuteCommand(command string, cwd string) (*types.Co
3737
// Create command using shell to properly handle complex commands
3838
var cmd *exec.Cmd
3939
if runtime.GOOS == "windows" {
40-
cmd = exec.Command("cmd", "/c", command)
40+
cmd = exec.Command("powershell", "-NoProfile", "-Command", command)
4141
} else {
4242
cmd = exec.Command("sh", "-c", command)
4343
}

internal/ui/aichat.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"io"
77
"os/exec"
8+
"regexp"
9+
"runtime"
810
"strings"
911
"time"
1012

@@ -72,6 +74,7 @@ type AIChatPane struct {
7274
terminalInput string // Current input line being typed in terminal mode
7375
aiAvailable bool // Whether the AI service is reachable
7476
aiChecked bool // Whether the availability check has completed
77+
suggestedFile string // Filename suggested by AI for the current code block
7578
}
7679

7780
// AIResponseMsg is sent when AI response chunk is received.
@@ -335,10 +338,15 @@ func (a *AIChatPane) AddFixRequest(message string, filePath string) {
335338
// Updates the codeBlocks slice for use in copy mode.
336339
func (a *AIChatPane) extractCodeBlocks() {
337340
a.codeBlocks = []string{}
341+
a.suggestedFile = ""
338342
for _, msg := range a.messages {
339343
if msg.Role == "assistant" {
340344
blocks := extractCodeFromMarkdown(msg.Content)
341345
a.codeBlocks = append(a.codeBlocks, blocks...)
346+
// Extract suggested filename from the last assistant message that has one
347+
if name := extractSuggestedFilename(msg.Content); name != "" {
348+
a.suggestedFile = name
349+
}
342350
}
343351
}
344352
}
@@ -377,6 +385,18 @@ func extractCodeFromMarkdown(content string) []string {
377385

378386
return blocks
379387
}
388+
// extractSuggestedFilename looks for a filename suggested by the AI in the response text.
389+
// It searches for patterns like `filename.sh`, "filename.sh", or filename.ext near
390+
// keywords like "save it to", "save it as", "for example", "called", "named".
391+
var suggestedFileRe = regexp.MustCompile("(?i)(?:save (?:it )?(?:to|as)(?: a file)?|for example|called|named|create(?: a file)?)[^`\"]{0,30}[`\"]([\\w/-]+\\.[a-z0-9]{1,4})[`\"]")
392+
393+
func extractSuggestedFilename(content string) string {
394+
matches := suggestedFileRe.FindStringSubmatch(content)
395+
if len(matches) >= 2 {
396+
return matches[1]
397+
}
398+
return ""
399+
}
380400

381401
// ClearHistory clears the conversation history.
382402
// Resets messages and scroll offset. Used for "New Chat" functionality (Ctrl+T).
@@ -451,7 +471,12 @@ func (a *AIChatPane) executeCommand(script string) tea.Cmd {
451471
outChan := make(chan tea.Msg)
452472

453473
go func() {
454-
cmd := exec.Command("sh", "-c", script)
474+
var cmd *exec.Cmd
475+
if runtime.GOOS == "windows" {
476+
cmd = exec.Command("powershell", "-NoProfile", "-Command", script)
477+
} else {
478+
cmd = exec.Command("sh", "-c", script)
479+
}
455480

456481
stdin, _ := cmd.StdinPipe()
457482
stdout, _ := cmd.StdoutPipe()
@@ -1560,6 +1585,10 @@ func (a *AIChatPane) GetCodeBlocks() []string {
15601585
func (a *AIChatPane) GetSelectedCodeBlock() string {
15611586
return a.lastSelectedCode
15621587
}
1588+
// GetSuggestedFilename returns the filename the AI suggested for the code, if any.
1589+
func (a *AIChatPane) GetSuggestedFilename() string {
1590+
return a.suggestedFile
1591+
}
15631592

15641593
// IsInViewMode returns whether the pane is in view mode.
15651594
// Used by App to show appropriate status bar instructions.
@@ -1586,6 +1615,28 @@ func (a *AIChatPane) GetLastAssistantResponse() string {
15861615
return ""
15871616
}
15881617

1618+
// RunScript enters terminal mode and executes the given command, streaming
1619+
// output into the AI chat pane. Returns a tea.Cmd to start the execution.
1620+
func (a *AIChatPane) RunScript(command string, label string) tea.Cmd {
1621+
if a.cmdRunning {
1622+
return nil
1623+
}
1624+
1625+
a.viewMode = true
1626+
a.terminalMode = true
1627+
a.cmdRunning = true
1628+
a.copyMode = false
1629+
1630+
a.terminalOutput = []string{
1631+
"▶ Running: " + label,
1632+
"> " + command,
1633+
"",
1634+
}
1635+
a.viewModeScroll = 0
1636+
1637+
return a.executeCommand(command)
1638+
}
1639+
15891640
// EnterConfigMode enters the configuration editor mode.
15901641
// Loads current config values and displays them for editing.
15911642
//

internal/ui/app.go

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"fmt"
1818
"os"
1919
"path/filepath"
20+
"runtime"
2021
"strings"
2122

2223
"github.com/atotto/clipboard"
@@ -289,11 +290,20 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
289290
a.editorPane.focused = true
290291
a.aiPane.focused = false
291292
} else {
292-
// No file open, prompt for new file
293-
a.pendingCodeInsert = selectedCode
294-
a.showFilePrompt = true
295-
a.filePromptBuffer = ""
296-
a.statusMessage = "Enter filename to insert code"
293+
// No file open — load code into editor as unsaved buffer
294+
suggestedName := a.aiPane.GetSuggestedFilename()
295+
a.editorPane.SetContentUnsaved(selectedCode, suggestedName)
296+
297+
// Switch to editor pane
298+
a.activePane = types.EditorPaneType
299+
a.editorPane.focused = true
300+
a.aiPane.focused = false
301+
302+
if suggestedName != "" {
303+
a.statusMessage = "Code loaded (suggested name: " + suggestedName + ") — Ctrl+S to save"
304+
} else {
305+
a.statusMessage = "Code loaded — Ctrl+S to save with a filename"
306+
}
297307
}
298308
} else {
299309
a.statusMessage = "No code block selected"
@@ -718,11 +728,33 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
718728
case "ctrl+s":
719729
// Save file in editor
720730
if a.activePane == types.EditorPaneType {
721-
err := a.editorPane.SaveFile()
722-
if err != nil {
723-
a.statusMessage = "Error saving: " + err.Error()
731+
if a.editorPane.currentFile == nil && a.editorPane.GetContent() != "" {
732+
// No file yet — check for AI-suggested name
733+
suggested := a.editorPane.GetSuggestedName()
734+
if suggested != "" {
735+
// Use the suggested name directly
736+
filePath := suggested
737+
err := a.fileManager.CreateFile(filePath, a.editorPane.GetContent())
738+
if err != nil {
739+
a.statusMessage = "Error creating file: " + err.Error()
740+
} else {
741+
a.editorPane.LoadFile(filePath)
742+
a.statusMessage = "Saved as " + filePath
743+
}
744+
} else {
745+
// No suggestion — prompt for filename
746+
a.pendingCodeInsert = a.editorPane.GetContent()
747+
a.showFilePrompt = true
748+
a.filePromptBuffer = ""
749+
a.statusMessage = "Enter filename to save"
750+
}
724751
} else {
725-
a.statusMessage = "File saved"
752+
err := a.editorPane.SaveFile()
753+
if err != nil {
754+
a.statusMessage = "Error saving: " + err.Error()
755+
} else {
756+
a.statusMessage = "File saved"
757+
}
726758
}
727759
}
728760
return a, nil
@@ -738,10 +770,56 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
738770
return a, nil
739771

740772
case "ctrl+r":
741-
// Execute script (placeholder for now)
742-
// TODO: Implement script execution
743-
a.statusMessage = "Script execution not yet implemented"
744-
return a, nil
773+
// Execute current script from editor
774+
if a.editorPane.currentFile == nil {
775+
a.statusMessage = "No file open to run"
776+
return a, nil
777+
}
778+
779+
filePath := a.editorPane.currentFile.Filepath
780+
fileType := a.editorPane.currentFile.FileType
781+
782+
// Auto-save before running
783+
if a.editorPane.HasUnsavedChanges() {
784+
if err := a.editorPane.SaveFile(); err != nil {
785+
a.statusMessage = "Save failed: " + err.Error()
786+
return a, nil
787+
}
788+
}
789+
790+
// Build the execution command based on file type
791+
var runCmd string
792+
switch fileType {
793+
case "bash":
794+
runCmd = "bash " + filePath
795+
case "powershell":
796+
if runtime.GOOS == "windows" {
797+
runCmd = "powershell -NoProfile -File " + filePath
798+
} else {
799+
runCmd = "pwsh -NoProfile -File " + filePath
800+
}
801+
case "python":
802+
runCmd = "python3 " + filePath
803+
default:
804+
// Default: try to run as shell script
805+
if runtime.GOOS == "windows" {
806+
runCmd = "powershell -NoProfile -File " + filePath
807+
} else {
808+
runCmd = "sh " + filePath
809+
}
810+
}
811+
812+
// Switch focus to AI pane and run
813+
a.activePane = types.AIPaneType
814+
a.editorPane.focused = false
815+
a.aiPane.focused = true
816+
817+
fileName := filepath.Base(filePath)
818+
a.statusMessage = "Running " + fileName + "..."
819+
820+
cmd := a.aiPane.RunScript(runCmd, fileName)
821+
cmds = append(cmds, cmd)
822+
return a, tea.Batch(cmds...)
745823

746824
case "ctrl+enter":
747825
// Send AI message with context from editor

internal/ui/editor.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type EditorPane struct {
4848
undoStack []editorSnapshot // Undo history
4949
redoStack []editorSnapshot // Redo history
5050
pendingAltD bool // Waiting for second key after Alt+D
51+
suggestedName string // AI-suggested filename for unsaved buffer
5152
}
5253

5354
// editorSnapshot stores editor state for undo/redo
@@ -215,6 +216,20 @@ func (e *EditorPane) SetContent(content string) {
215216
e.currentFile.IsModified = (e.content != e.originalContent) || len(e.diffMarkers) > 0
216217
}
217218
}
219+
// SetContentUnsaved loads content into the editor without an associated file.
220+
// If suggestedName is provided, it will be used as the default filename on save.
221+
func (e *EditorPane) SetContentUnsaved(content string, suggestedName string) {
222+
e.content = content
223+
e.originalContent = ""
224+
e.cursorLine = 0
225+
e.cursorCol = 0
226+
e.scrollOffset = 0
227+
e.currentFile = nil
228+
e.diffMarkers = make(map[int]string)
229+
e.suggestedName = suggestedName
230+
e.undoStack = nil
231+
e.redoStack = nil
232+
}
218233

219234
// HasUnsavedChanges checks if editor has unsaved changes.
220235
// Compares current content with originalContent (content at last save/load).
@@ -958,3 +973,7 @@ func (e *EditorPane) GetCurrentFile() *FileContext {
958973
FileType: e.currentFile.FileType,
959974
}
960975
}
976+
// GetSuggestedName returns the AI-suggested filename, if any.
977+
func (e *EditorPane) GetSuggestedName() string {
978+
return e.suggestedName
979+
}

internal/ui/help.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func (a *App) renderHelpDialog() string {
3737
helpText += keyStyle.Render(" Ctrl+N") + descStyle.Render(" New file") + "\n"
3838
helpText += keyStyle.Render(" Ctrl+S") + descStyle.Render(" Save file") + "\n"
3939
helpText += keyStyle.Render(" Ctrl+X") + descStyle.Render(" Close file") + "\n"
40+
helpText += keyStyle.Render(" Ctrl+R") + descStyle.Render(" Run current script") + "\n"
4041
helpText += keyStyle.Render(" Ctrl+B") + descStyle.Render(" Backup Picker (Restore previous versions)") + "\n"
4142
helpText += keyStyle.Render(" Ctrl+Q") + descStyle.Render(" Quit") + "\n"
4243
helpText += "\n"

0 commit comments

Comments
 (0)