Skip to content

Commit c2c0e2f

Browse files
authored
Merge pull request #275 from shelltime/revert/pr-273
revert(cli): undo PR #273 AI response parsing changes
2 parents 2081844 + 5a30d5c commit c2c0e2f

4 files changed

Lines changed: 12 additions & 222 deletions

File tree

commands/query.go

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,7 @@ func commandQuery(c *cli.Context) error {
102102
// Print newline after streaming
103103
fmt.Println()
104104

105-
newCommand := sanitizeSuggestedCommand(result.String())
106-
if newCommand == "" {
107-
color.Red.Println("AI returned an empty response. Try rephrasing your query.")
108-
return fmt.Errorf("empty AI response")
109-
}
105+
newCommand := strings.TrimSpace(result.String())
110106

111107
// Check auto-run configuration
112108
if cfg.AI != nil && (cfg.AI.Agent.View || cfg.AI.Agent.Edit || cfg.AI.Agent.Delete) {
@@ -192,39 +188,6 @@ func executeCommand(ctx context.Context, command string) error {
192188
return nil
193189
}
194190

195-
// sanitizeSuggestedCommand normalizes raw AI output into an executable command.
196-
// It strips triple-backtick fences (with optional language tag like bash, sh,
197-
// zsh, fish, pwsh, powershell), strips surrounding single backticks when the
198-
// result is a single line, and trims whitespace. Responses that start with `#`
199-
// are treated as refusal comments and preserved verbatim so the caller can
200-
// surface them to the user without attempting execution.
201-
func sanitizeSuggestedCommand(raw string) string {
202-
s := strings.TrimSpace(raw)
203-
if s == "" {
204-
return ""
205-
}
206-
207-
if strings.HasPrefix(s, "```") {
208-
s = strings.TrimPrefix(s, "```")
209-
if nl := strings.IndexByte(s, '\n'); nl >= 0 {
210-
switch strings.ToLower(strings.TrimSpace(s[:nl])) {
211-
case "", "bash", "sh", "shell", "zsh", "fish", "pwsh", "powershell":
212-
s = s[nl+1:]
213-
}
214-
}
215-
s = strings.TrimRight(s, " \t\n")
216-
s = strings.TrimSuffix(s, "```")
217-
s = strings.TrimSpace(s)
218-
}
219-
220-
if !strings.ContainsRune(s, '\n') && len(s) >= 2 &&
221-
strings.HasPrefix(s, "`") && strings.HasSuffix(s, "`") {
222-
s = strings.TrimSpace(s[1 : len(s)-1])
223-
}
224-
225-
return s
226-
}
227-
228191
func getSystemContext(query string, ai *model.AIConfig) (model.CommandSuggestVariables, error) {
229192
// Get shell information
230193
shell := os.Getenv("SHELL")

commands/query_test.go

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -458,42 +458,7 @@ func (s *queryTestSuite) TestQueryCommandEmptyAIResponse() {
458458
}
459459

460460
err := s.app.Run(command)
461-
assert.NotNil(s.T(), err)
462-
assert.Contains(s.T(), err.Error(), "empty AI response")
463-
}
464-
465-
func (s *queryTestSuite) TestSanitizeSuggestedCommand() {
466-
tests := []struct {
467-
name string
468-
in string
469-
want string
470-
}{
471-
{"plain", "ls -la", "ls -la"},
472-
{"trims whitespace", " ls -la \n\t", "ls -la"},
473-
{"fence with bash tag", "```bash\necho hi\n```", "echo hi"},
474-
{"fence with sh tag", "```sh\necho hi\n```", "echo hi"},
475-
{"fence with zsh tag", "```zsh\necho hi\n```", "echo hi"},
476-
{"fence with shell tag", "```shell\necho hi\n```", "echo hi"},
477-
{"fence with fish tag", "```fish\nset -x FOO bar\n```", "set -x FOO bar"},
478-
{"fence with powershell tag", "```powershell\nGet-Process\n```", "Get-Process"},
479-
{"fence with pwsh tag", "```pwsh\nGet-Process\n```", "Get-Process"},
480-
{"fence no language tag", "```\necho hi\n```", "echo hi"},
481-
{"fence with trailing newline before closing", "```bash\nls -la\n\n```", "ls -la"},
482-
{"single backticks around single-line", "`ls -la`", "ls -la"},
483-
{"single backticks with surrounding space", " `ls -la` ", "ls -la"},
484-
{"only whitespace", " \n\t ", ""},
485-
{"empty", "", ""},
486-
{"comment passthrough preserved", "# refusing: unsafe request", "# refusing: unsafe request"},
487-
{"multiline without fences kept", "ls\ncat foo", "ls\ncat foo"},
488-
}
489-
for _, tt := range tests {
490-
s.T().Run(tt.name, func(t *testing.T) {
491-
got := sanitizeSuggestedCommand(tt.in)
492-
if got != tt.want {
493-
t.Errorf("sanitizeSuggestedCommand(%q) = %q, want %q", tt.in, got, tt.want)
494-
}
495-
})
496-
}
461+
assert.Nil(s.T(), err)
497462
}
498463

499464
func (s *queryTestSuite) TestQueryCommandDescription() {

model/ai_service.go

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -74,26 +74,24 @@ func (s *sseAIService) QueryCommandStream(
7474
for scanner.Scan() {
7575
line := scanner.Text()
7676

77-
if line == "" {
78-
isError = false
77+
if line == "event: error" {
78+
isError = true
7979
continue
8080
}
8181

82-
if v, ok := stripSSEField(line, "event:"); ok {
83-
if v == "error" {
84-
isError = true
85-
}
86-
continue
87-
}
82+
if strings.HasPrefix(line, "data:") {
83+
data := line[len("data:"):]
8884

89-
if v, ok := stripSSEField(line, "data:"); ok {
9085
if isError {
91-
return fmt.Errorf("server error: %s", v)
86+
return fmt.Errorf("server error: %s", data)
9287
}
93-
if v == "[DONE]" {
88+
89+
if data == "[DONE]" {
9490
return nil
9591
}
96-
onToken(v)
92+
93+
onToken(data)
94+
isError = false
9795
}
9896
}
9997

@@ -103,17 +101,3 @@ func (s *sseAIService) QueryCommandStream(
103101

104102
return nil
105103
}
106-
107-
// stripSSEField returns the value after prefix, stripping one optional leading
108-
// space per the SSE specification (§9.2 "If value starts with a U+0020 SPACE
109-
// character, remove it from value").
110-
func stripSSEField(line, prefix string) (string, bool) {
111-
if !strings.HasPrefix(line, prefix) {
112-
return "", false
113-
}
114-
v := line[len(prefix):]
115-
if strings.HasPrefix(v, " ") {
116-
v = v[1:]
117-
}
118-
return v, true
119-
}

model/ai_service_test.go

Lines changed: 0 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"fmt"
77
"net/http"
88
"net/http/httptest"
9-
"strings"
109
"testing"
1110
)
1211

@@ -91,124 +90,3 @@ func TestQueryCommandStream_ErrorResponseBody(t *testing.T) {
9190
})
9291
}
9392
}
94-
95-
func TestQueryCommandStream_SSEParsing(t *testing.T) {
96-
tests := []struct {
97-
name string
98-
body string
99-
wantErr bool
100-
wantErrSubstr string
101-
wantTokens []string
102-
}{
103-
{
104-
name: "data with space and [DONE] terminates cleanly",
105-
body: "data: [DONE]\n\n",
106-
wantTokens: nil,
107-
},
108-
{
109-
name: "data without space and [DONE] terminates cleanly",
110-
body: "data:[DONE]\n\n",
111-
wantTokens: nil,
112-
},
113-
{
114-
name: "single data token with leading space is stripped",
115-
body: "data: hello\n\ndata: [DONE]\n\n",
116-
wantTokens: []string{"hello"},
117-
},
118-
{
119-
name: "single data token without leading space passes through",
120-
body: "data:hello\n\ndata:[DONE]\n\n",
121-
wantTokens: []string{"hello"},
122-
},
123-
{
124-
name: "multi-token stream concatenates without spurious spaces",
125-
body: "data: ls\n\ndata: -la\n\ndata: [DONE]\n\n",
126-
wantTokens: []string{"ls", " -la"},
127-
},
128-
{
129-
name: "event error with space",
130-
body: "event: error\ndata: boom\n\n",
131-
wantErr: true,
132-
wantErrSubstr: "boom",
133-
},
134-
{
135-
name: "event error without space",
136-
body: "event:error\ndata:boom\n\n",
137-
wantErr: true,
138-
wantErrSubstr: "boom",
139-
},
140-
{
141-
name: "blank line resets error state between events",
142-
body: "event: error\n\ndata: hello\n\ndata: [DONE]\n\n",
143-
wantTokens: []string{"hello"},
144-
},
145-
}
146-
147-
for _, tt := range tests {
148-
t.Run(tt.name, func(t *testing.T) {
149-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
150-
w.Header().Set("Content-Type", "text/event-stream")
151-
w.WriteHeader(http.StatusOK)
152-
_, _ = w.Write([]byte(tt.body))
153-
}))
154-
defer server.Close()
155-
156-
var got []string
157-
svc := NewAIService()
158-
err := svc.QueryCommandStream(
159-
context.Background(),
160-
CommandSuggestVariables{Shell: "bash", Os: "linux", Query: "test"},
161-
Endpoint{APIEndpoint: server.URL, Token: "test-token"},
162-
func(token string) { got = append(got, token) },
163-
)
164-
165-
if tt.wantErr {
166-
if err == nil {
167-
t.Fatalf("expected error, got nil (tokens=%v)", got)
168-
}
169-
if !strings.Contains(err.Error(), tt.wantErrSubstr) {
170-
t.Fatalf("expected error to contain %q, got %q", tt.wantErrSubstr, err.Error())
171-
}
172-
return
173-
}
174-
175-
if err != nil {
176-
t.Fatalf("unexpected error: %v", err)
177-
}
178-
if len(got) != len(tt.wantTokens) {
179-
t.Fatalf("token count mismatch: want %d %v, got %d %v", len(tt.wantTokens), tt.wantTokens, len(got), got)
180-
}
181-
for i, tok := range tt.wantTokens {
182-
if got[i] != tok {
183-
t.Errorf("token[%d] = %q, want %q", i, got[i], tok)
184-
}
185-
}
186-
})
187-
}
188-
}
189-
190-
func TestStripSSEField(t *testing.T) {
191-
tests := []struct {
192-
name string
193-
line string
194-
prefix string
195-
wantVal string
196-
wantOk bool
197-
}{
198-
{"no match", "foo:bar", "data:", "", false},
199-
{"match no space", "data:hello", "data:", "hello", true},
200-
{"match one space stripped", "data: hello", "data:", "hello", true},
201-
{"match two spaces preserves second", "data: hello", "data:", " hello", true},
202-
{"empty value no space", "data:", "data:", "", true},
203-
{"empty value one space", "data: ", "data:", "", true},
204-
{"event error with space", "event: error", "event:", "error", true},
205-
}
206-
for _, tt := range tests {
207-
t.Run(tt.name, func(t *testing.T) {
208-
v, ok := stripSSEField(tt.line, tt.prefix)
209-
if ok != tt.wantOk || v != tt.wantVal {
210-
t.Errorf("stripSSEField(%q, %q) = (%q, %v), want (%q, %v)", tt.line, tt.prefix, v, ok, tt.wantVal, tt.wantOk)
211-
}
212-
})
213-
}
214-
}

0 commit comments

Comments
 (0)