diff --git a/README.md b/README.md index 5b19421..93eb53d 100644 --- a/README.md +++ b/README.md @@ -101,17 +101,21 @@ codeye --agent "my-custom-agent --stdio" prompt "review security po ### Global Options -| Flag | Description | -| ----------------- | ----------------------------------------------------------- | -| `--cwd ` | Run in a specific working directory | -| `--agent ""` | Use a custom ACP-compatible agent command | -| `--format ` | Output format: `text`, `json`, `json-strict`, `quiet` | -| `--json-strict` | Shorthand for `--format json-strict` | -| `--approve-all` | Allow all agent tool requests | -| `--approve-reads` | Allow read-only tool requests, deny writes | -| `--deny-all` | Deny all agent tool requests | -| `--ask` | Prompt to approve or reject each tool request (interactive) | -| `--version`, `-V` | Print version | +| Flag | Description | +| ------------------- | ----------------------------------------------------------- | +| `--cwd ` | Run in a specific working directory | +| `--agent ""` | Use a custom ACP-compatible agent command | +| `--format ` | Output format: `text`, `json`, `json-strict`, `quiet` | +| `--json-strict` | Shorthand for `--format json-strict` | +| `--audio ` | Add audio file(s) to prompt/exec (repeatable). Supports .wav, .mp3, .ogg, .flac, .m4a | +| `--image ` | Add image file(s) to prompt/exec (repeatable). Supports .png, .jpg, .gif, .webp | +| `--approve-all` | Allow all agent tool requests | +| `--approve-reads` | Allow read-only tool requests, deny writes | +| `--deny-all` | Deny all agent tool requests | +| `--ask` | Prompt to approve or reject each tool request (interactive) | +| `--version`, `-V` | Print version | + +For `prompt` and `exec`, place `--audio` and `--image` before the command (e.g. `codeye --image diagram.png prompt "describe this"`). Agents must advertise the corresponding prompt capabilities (image/audio) in initialization. ## Configuration diff --git a/internal/acp/payloads.go b/internal/acp/payloads.go index 34efb8b..e042726 100644 --- a/internal/acp/payloads.go +++ b/internal/acp/payloads.go @@ -46,14 +46,23 @@ type SessionListResponse struct { Sessions []SessionListEntry `json:"sessions"` } -type PromptTextPart struct { - Type string `json:"type"` - Text string `json:"text"` +// PromptPart is one content block in a session/prompt request. +// For text: Type="text", Text set. For image/audio: Type="image"|"audio", MimeType and Data (base64) set. +type PromptPart struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + MimeType string `json:"mimeType,omitempty"` + Data string `json:"data,omitempty"` // base64-encoded for image/audio +} + +// TextPrompt returns a single text prompt part (convenience for callers that have only text). +func TextPrompt(text string) []PromptPart { + return []PromptPart{{Type: "text", Text: text}} } type SessionPromptRequest struct { - SessionID string `json:"sessionId"` - Prompt []PromptTextPart `json:"prompt"` + SessionID string `json:"sessionId"` + Prompt []PromptPart `json:"prompt"` } type SessionPromptResponse struct { diff --git a/internal/cli/prompt_parts.go b/internal/cli/prompt_parts.go new file mode 100644 index 0000000..ae3500e --- /dev/null +++ b/internal/cli/prompt_parts.go @@ -0,0 +1,69 @@ +package cli + +import ( + "encoding/base64" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/one710/codeye/internal/acp" +) + +// BuildPromptParts returns ACP prompt parts: one text block (if text is non-empty), then image blocks, then audio blocks. +// Image and audio files are read and base64-encoded; mime types are inferred from extension. +func BuildPromptParts(text string, imagePaths, audioPaths []string) ([]acp.PromptPart, error) { + var parts []acp.PromptPart + text = strings.TrimSpace(text) + if text != "" { + parts = append(parts, acp.PromptPart{Type: "text", Text: text}) + } + for _, p := range imagePaths { + mime, data, err := readFileAsBase64(p, imageMimeTypes) + if err != nil { + return nil, fmt.Errorf("image %s: %w", p, err) + } + parts = append(parts, acp.PromptPart{Type: "image", MimeType: mime, Data: data}) + } + for _, p := range audioPaths { + mime, data, err := readFileAsBase64(p, audioMimeTypes) + if err != nil { + return nil, fmt.Errorf("audio %s: %w", p, err) + } + parts = append(parts, acp.PromptPart{Type: "audio", MimeType: mime, Data: data}) + } + if len(parts) == 0 { + parts = []acp.PromptPart{{Type: "text", Text: ""}} + } + return parts, nil +} + +func readFileAsBase64(path string, mimeMap map[string]string) (mimeType, b64 string, err error) { + ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(path), ".")) + mimeType = mimeMap[ext] + if mimeType == "" { + return "", "", fmt.Errorf("unsupported extension .%s", ext) + } + raw, err := os.ReadFile(path) + if err != nil { + return "", "", err + } + return mimeType, base64.StdEncoding.EncodeToString(raw), nil +} + +var imageMimeTypes = map[string]string{ + "png": "image/png", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "gif": "image/gif", + "webp": "image/webp", +} + +var audioMimeTypes = map[string]string{ + "wav": "audio/wav", + "mp3": "audio/mpeg", + "mpeg": "audio/mpeg", + "ogg": "audio/ogg", + "flac": "audio/flac", + "m4a": "audio/mp4", +} diff --git a/internal/cli/root.go b/internal/cli/root.go index c7bc24e..11bec03 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -32,6 +32,8 @@ type globalFlags struct { AgentCommand string Format output.Format PermMode permissions.Mode + AudioPaths []string + ImagePaths []string } func Run(argv []string) int { @@ -114,7 +116,7 @@ func Run(argv []string) int { ) ctx := context.Background() - code := dispatch(ctx, out, repo, rt, adapter, agent, flags.Cwd, cmd, cmdArgs, flags.PermMode, flags.Format) + code := dispatch(ctx, out, repo, rt, adapter, agent, flags.Cwd, cmd, cmdArgs, flags.PermMode, flags.Format, flags.AudioPaths, flags.ImagePaths) return code } @@ -130,15 +132,21 @@ func dispatch( args []string, permMode permissions.Mode, format output.Format, + audioPaths, imagePaths []string, ) int { switch cmd { case "prompt": if len(args) < 2 { - out.PrintRPCError(-32602, "usage: prompt ", map[string]interface{}{"errorCode": cerr.CodeUsage}) + out.PrintRPCError(-32602, "usage: prompt [--audio ...] [--image ...]", map[string]interface{}{"errorCode": cerr.CodeUsage}) return 2 } sessionID := args[0] promptText := strings.Join(args[1:], " ") + parts, err := BuildPromptParts(promptText, imagePaths, audioPaths) + if err != nil { + out.PrintError(err.Error()) + return 1 + } rec, err := repo.Load(sessionID) if err != nil { out.PrintError("session not found: " + sessionID) @@ -149,7 +157,7 @@ func dispatch( return 1 } // ACP: session/load (by agent) restores conversation history; then session/prompt. All in-process. - stopReason, responseText, err := rt.PromptWithOutput(ctx, rec, promptText) + stopReason, responseText, err := rt.PromptWithOutput(ctx, rec, parts) if err != nil { out.PrintError(err.Error()) return 1 @@ -163,11 +171,16 @@ func dispatch( }, "") return 0 case "exec": - if len(args) == 0 { - out.PrintRPCError(-32602, "prompt is required", map[string]interface{}{"errorCode": cerr.CodeUsage}) + if len(args) == 0 && len(audioPaths) == 0 && len(imagePaths) == 0 { + out.PrintRPCError(-32602, "prompt text or --audio/--image is required", map[string]interface{}{"errorCode": cerr.CodeUsage}) return 2 } - stopReason, responseText, err := rt.RunOnceWithOutput(ctx, cwd, strings.Join(args, " ")) + parts, err := BuildPromptParts(strings.Join(args, " "), imagePaths, audioPaths) + if err != nil { + out.PrintError(err.Error()) + return 1 + } + stopReason, responseText, err := rt.RunOnceWithOutput(ctx, cwd, parts) if err != nil { out.PrintError(err.Error()) return 1 @@ -428,6 +441,16 @@ func parseGlobals(args []string) (globalFlags, []string, error) { flags.PermMode = permissions.DenyAll case "--ask": flags.PermMode = permissions.Ask + case "--audio": + i++ + if i < len(args) { + flags.AudioPaths = append(flags.AudioPaths, args[i]) + } + case "--image": + i++ + if i < len(args) { + flags.ImagePaths = append(flags.ImagePaths, args[i]) + } default: if strings.HasPrefix(arg, "--") { return flags, nil, fmt.Errorf("unknown flag: %s", arg) diff --git a/internal/client/client.go b/internal/client/client.go index abb3a7a..6c07e12 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -242,7 +242,10 @@ func (c *Client) ListSessions(ctx context.Context, cwd string) ([]string, error) return out, nil } -func (c *Client) Prompt(ctx context.Context, sessionID, text string) (PromptResult, error) { +func (c *Client) Prompt(ctx context.Context, sessionID string, parts []acp.PromptPart) (PromptResult, error) { + if len(parts) == 0 { + parts = []acp.PromptPart{{Type: "text", Text: ""}} + } c.updateMu.Lock() c.activePromptSessionID = sessionID c.activePromptChunks = nil @@ -256,9 +259,7 @@ func (c *Client) Prompt(ctx context.Context, sessionID, text string) (PromptResu res, err := c.call(ctx, acp.MethodSessionPrompt, acp.SessionPromptRequest{ SessionID: sessionID, - Prompt: []acp.PromptTextPart{ - {Type: "text", Text: text}, - }, + Prompt: parts, }) if err != nil { return PromptResult{}, err diff --git a/internal/queue/protocol.go b/internal/queue/protocol.go index 439e83c..9bd2e02 100644 --- a/internal/queue/protocol.go +++ b/internal/queue/protocol.go @@ -1,5 +1,7 @@ package queue +import "github.com/one710/codeye/internal/acp" + type Command string const ( @@ -11,13 +13,14 @@ const ( ) type Request struct { - RequestID string `json:"requestId,omitempty"` - Command Command `json:"command"` - SessionID string `json:"sessionId"` - Prompt string `json:"prompt,omitempty"` - Mode string `json:"mode,omitempty"` - Key string `json:"key,omitempty"` - Value string `json:"value,omitempty"` + RequestID string `json:"requestId,omitempty"` + Command Command `json:"command"` + SessionID string `json:"sessionId"` + Prompt string `json:"prompt,omitempty"` // legacy: single text part + PromptParts []acp.PromptPart `json:"promptParts,omitempty"` // when set, used instead of Prompt + Mode string `json:"mode,omitempty"` + Key string `json:"key,omitempty"` + Value string `json:"value,omitempty"` } type Response struct { diff --git a/internal/queue/server.go b/internal/queue/server.go index d53a4e1..4a56233 100644 --- a/internal/queue/server.go +++ b/internal/queue/server.go @@ -8,10 +8,12 @@ import ( "os" "sync" "time" + + "github.com/one710/codeye/internal/acp" ) type Handler interface { - Prompt(ctx context.Context, sessionID, prompt string) (PromptResult, error) + Prompt(ctx context.Context, sessionID string, parts []acp.PromptPart) (PromptResult, error) Cancel(ctx context.Context, sessionID string) error SetMode(ctx context.Context, sessionID, mode string) error SetConfigOption(ctx context.Context, sessionID, key, value string) error @@ -122,7 +124,11 @@ func (s *Server) Run(ctx context.Context) error { func (s *Server) dispatch(ctx context.Context, req Request) (Response, error) { switch req.Command { case CmdPrompt: - result, err := s.Handler.Prompt(ctx, req.SessionID, req.Prompt) + parts := req.PromptParts + if len(parts) == 0 { + parts = []acp.PromptPart{{Type: "text", Text: req.Prompt}} + } + result, err := s.Handler.Prompt(ctx, req.SessionID, parts) if err != nil { return Response{}, err } diff --git a/internal/session/runtime.go b/internal/session/runtime.go index 246a439..aea4b93 100644 --- a/internal/session/runtime.go +++ b/internal/session/runtime.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/one710/codeye/internal/acp" "github.com/one710/codeye/internal/client" "github.com/one710/codeye/internal/queue" "github.com/one710/codeye/internal/session/persistence" @@ -59,18 +60,18 @@ func (r *Runtime) CreateSession(ctx context.Context, agent, cwd, name string) (p return rec, nil } -func (r *Runtime) Prompt(ctx context.Context, rec persistence.Record, prompt string) (string, error) { - stopReason, _, err := r.PromptWithOutput(ctx, rec, prompt) +func (r *Runtime) Prompt(ctx context.Context, rec persistence.Record, parts []acp.PromptPart) (string, error) { + stopReason, _, err := r.PromptWithOutput(ctx, rec, parts) return stopReason, err } -func (r *Runtime) PromptWithOutput(ctx context.Context, rec persistence.Record, prompt string) (string, string, error) { - return r.PromptInProcess(ctx, rec, prompt) +func (r *Runtime) PromptWithOutput(ctx context.Context, rec persistence.Record, parts []acp.PromptPart) (string, string, error) { + return r.PromptInProcess(ctx, rec, parts) } // PromptInProcess runs the prompt in the current process with the same client as exec, // so streaming and permission prompts behave identically to exec. -func (r *Runtime) PromptInProcess(ctx context.Context, rec persistence.Record, prompt string) (string, string, error) { +func (r *Runtime) PromptInProcess(ctx context.Context, rec persistence.Record, parts []acp.PromptPart) (string, string, error) { c := r.ClientFactory() if err := c.Start(ctx); err != nil { return "", "", err @@ -86,7 +87,7 @@ func (r *Runtime) PromptInProcess(ctx context.Context, rec persistence.Record, p rec.ACPSession = newSid _ = r.Repo.Save(rec) } - result, err := c.Prompt(ctx, sid, prompt) + result, err := c.Prompt(ctx, sid, parts) if err != nil { return "", "", err } @@ -193,12 +194,12 @@ func (r *Runtime) RunWorkingSession(ctx context.Context, initial persistence.Rec return server.Run(ctx) } -func (r *Runtime) RunOnce(ctx context.Context, cwd, prompt string) (string, error) { - stopReason, _, err := r.RunOnceWithOutput(ctx, cwd, prompt) +func (r *Runtime) RunOnce(ctx context.Context, cwd string, parts []acp.PromptPart) (string, error) { + stopReason, _, err := r.RunOnceWithOutput(ctx, cwd, parts) return stopReason, err } -func (r *Runtime) RunOnceWithOutput(ctx context.Context, cwd, prompt string) (string, string, error) { +func (r *Runtime) RunOnceWithOutput(ctx context.Context, cwd string, parts []acp.PromptPart) (string, string, error) { c := r.ClientFactory() if err := c.Start(ctx); err != nil { return "", "", err @@ -208,7 +209,7 @@ func (r *Runtime) RunOnceWithOutput(ctx context.Context, cwd, prompt string) (st if err != nil { return "", "", err } - result, err := c.Prompt(ctx, sid, prompt) + result, err := c.Prompt(ctx, sid, parts) if err != nil { return "", "", err } @@ -250,14 +251,14 @@ type workingSessionHandler struct { fullMessage string // set when we get a non-chunk final message to avoid duplicating chunk content } -func (w *workingSessionHandler) Prompt(ctx context.Context, sessionID, prompt string) (queue.PromptResult, error) { +func (w *workingSessionHandler) Prompt(ctx context.Context, sessionID string, parts []acp.PromptPart) (queue.PromptResult, error) { w.mu.Lock() activeSessionID := w.liveSessionID w.chunks = nil w.fullMessage = "" w.mu.Unlock() - result, err := w.client.Prompt(ctx, activeSessionID, prompt) + result, err := w.client.Prompt(ctx, activeSessionID, parts) if err != nil && shouldRecreateSessionOnPromptError(err) { // Agent rejected stale/unknown session; recreate once and retry. if sid, createErr := w.client.CreateSession(ctx, w.cwd); createErr == nil { @@ -268,7 +269,7 @@ func (w *workingSessionHandler) Prompt(ctx context.Context, sessionID, prompt st _ = w.repo.Save(w.record) } w.mu.Unlock() - result, err = w.client.Prompt(ctx, sid, prompt) + result, err = w.client.Prompt(ctx, sid, parts) } } if err != nil { diff --git a/skills/codeye/SKILL.md b/skills/codeye/SKILL.md index a49ad45..fe5ed39 100644 --- a/skills/codeye/SKILL.md +++ b/skills/codeye/SKILL.md @@ -55,6 +55,8 @@ codeye [global-options] [agent] [command-args] - `--agent ""`: custom ACP-compatible adapter command. - `--format `: output format. - `--json-strict`: alias for strict JSON output mode. +- `--audio `: add audio file to prompt/exec (repeatable; .wav, .mp3, .ogg, .flac, .m4a). +- `--image `: add image file to prompt/exec (repeatable; .png, .jpg, .gif, .webp). - `--approve-all`: allow all ACP tool requests. - `--approve-reads`: allow read-only tool requests, deny writes. - `--deny-all`: deny all ACP tool requests. @@ -104,6 +106,45 @@ codeye cursor exec "summarize this repository" codeye --agent "my-acp-adapter --stdio" exec "run a quick code review" ``` +## Image and audio in prompt/exec + +Place `--image` and `--audio` before the command. You can pass one or many files; they are sent as content blocks with the text prompt. + +**One image with text (exec):** + +```bash +codeye --image screenshot.png exec "what is shown in this screenshot? list the main UI elements" +``` + +**One image with text (prompt):** + +```bash +SID=$(codeye cursor sessions new | jq -r .sessionId) +codeye --image diagram.png cursor prompt $SID "explain this diagram and suggest improvements" +``` + +**Multiple images with text:** + +```bash +codeye --image before.png --image after.png exec "compare these two screenshots and describe what changed" +codeye --image fig1.png --image fig2.png cursor prompt $SID "summarize the flow in these two figures" +``` + +**Audio with text (e.g. transcription or analysis):** + +```bash +codeye --audio meeting.wav exec "transcribe this and list action items" +codeye --audio intro.mp3 --audio outro.mp3 cursor prompt $SID "do these two clips sound consistent in tone?" +``` + +**Image and audio together:** + +```bash +codeye --image slide.png --audio narration.wav exec "align this slide with the narration and suggest edits" +``` + +Supported image extensions: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`. Audio: `.wav`, `.mp3`, `.ogg`, `.flac`, `.m4a`. The agent must advertise the corresponding prompt capabilities (image/audio) in initialization. + ## Config file Global config path: `~/.codeye/config.json` diff --git a/test/agents_integration_test.go b/test/agents_integration_test.go index c43700c..bc07c98 100644 --- a/test/agents_integration_test.go +++ b/test/agents_integration_test.go @@ -16,6 +16,7 @@ import ( "testing" "time" + "github.com/one710/codeye/internal/acp" "github.com/one710/codeye/internal/client" "github.com/one710/codeye/internal/permissions" ) @@ -113,7 +114,7 @@ func TestAgentsIntegration_Exec(t *testing.T) { } ctxPrompt, cancelPrompt := context.WithTimeout(ctx, 2*time.Minute) defer cancelPrompt() - result, err := c.Prompt(ctxPrompt, sid, "Reply with exactly: OK") + result, err := c.Prompt(ctxPrompt, sid, acp.TextPrompt("Reply with exactly: OK")) if err != nil { t.Fatalf("Prompt: %v", err) } @@ -150,7 +151,7 @@ func TestAgentsIntegration_Prompt(t *testing.T) { } ctxPrompt, cancelPrompt := context.WithTimeout(ctx, 2*time.Minute) defer cancelPrompt() - _, err = c.Prompt(ctxPrompt, sid, "What is 2+2? Reply with one word.") + _, err = c.Prompt(ctxPrompt, sid, acp.TextPrompt("What is 2+2? Reply with one word.")) if err != nil { t.Fatalf("Prompt: %v", err) } @@ -186,13 +187,13 @@ func TestAgentsIntegration_FollowUpPrompt(t *testing.T) { defer cancelPrompt() // First prompt: establish context the agent must remember - _, err = c.Prompt(ctxPrompt, sid, "Remember this secret code: CODESESSION99. You will be asked for it next. Reply with OK.") + _, err = c.Prompt(ctxPrompt, sid, acp.TextPrompt("Remember this secret code: CODESESSION99. You will be asked for it next. Reply with OK.")) if err != nil { t.Fatalf("First prompt: %v", err) } // Second prompt: ask for the remembered context - result, err := c.Prompt(ctxPrompt, sid, "What was the secret code I asked you to remember? Reply with only the code, nothing else.") + result, err := c.Prompt(ctxPrompt, sid, acp.TextPrompt("What was the secret code I asked you to remember? Reply with only the code, nothing else.")) if err != nil { t.Fatalf("Follow-up prompt: %v", err) } @@ -266,7 +267,7 @@ func TestAgentsIntegration_PermissionModes(t *testing.T) { if err != nil { t.Fatalf("CreateSession: %v", err) } - result, err := c.Prompt(ctx, sid, "test-tools") + result, err := c.Prompt(ctx, sid, acp.TextPrompt("test-tools")) if err != nil { t.Fatalf("Prompt: %v", err) } diff --git a/test/client_edge_test.go b/test/client_edge_test.go index 2d561bc..29ed929 100644 --- a/test/client_edge_test.go +++ b/test/client_edge_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/one710/codeye/internal/acp" "github.com/one710/codeye/internal/client" "github.com/one710/codeye/internal/permissions" ) @@ -20,7 +21,7 @@ func TestClientPromptWithEmptyStopReason(t *testing.T) { } defer c.Close() sid, _ := c.CreateSession(ctx, t.TempDir()) - result, err := c.Prompt(ctx, sid, "hello") + result, err := c.Prompt(ctx, sid, acp.TextPrompt("hello")) if err != nil { t.Fatalf("Prompt: %v", err) } @@ -135,7 +136,7 @@ func TestClientToolHandlingDenyAll(t *testing.T) { c.Start(ctx) defer c.Close() sid, _ := c.CreateSession(ctx, toolDir) - result, err := c.Prompt(ctx, sid, "test-tools") + result, err := c.Prompt(ctx, sid, acp.TextPrompt("test-tools")) if err != nil { t.Fatalf("Prompt should succeed even with deny-all: %v", err) } diff --git a/test/client_integration_test.go b/test/client_integration_test.go index 0b2bd80..a0659d2 100644 --- a/test/client_integration_test.go +++ b/test/client_integration_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/one710/codeye/internal/acp" "github.com/one710/codeye/internal/client" "github.com/one710/codeye/internal/permissions" ) @@ -61,7 +62,7 @@ func TestClientLifecycleWithMockAgent(t *testing.T) { if sid == "" { t.Fatal("expected session id") } - result, err := c.Prompt(ctx, sid, "hello") + result, err := c.Prompt(ctx, sid, acp.TextPrompt("hello")) if err != nil { t.Fatalf("Prompt: %v", err) } @@ -88,7 +89,7 @@ func TestLoadSessionReplayDrainSuppressesReplayUpdates(t *testing.T) { if got := atomic.LoadInt32(&updates); got != 0 { t.Fatalf("expected replay updates suppressed, got %d", got) } - if _, err := c.Prompt(ctx, sid, "live"); err != nil { + if _, err := c.Prompt(ctx, sid, acp.TextPrompt("live")); err != nil { t.Fatalf("Prompt: %v", err) } if got := atomic.LoadInt32(&updates); got == 0 { @@ -228,7 +229,7 @@ func TestToolHandlingDuringPrompt(t *testing.T) { } defer c.Close() sid, _ := c.CreateSession(ctx, toolDir) - result, err := c.Prompt(ctx, sid, "test-tools") + result, err := c.Prompt(ctx, sid, acp.TextPrompt("test-tools")) if err != nil { t.Fatalf("Prompt with tools: %v", err) } @@ -256,7 +257,7 @@ func TestPromptEmptyStopReasonDefaultsToEndTurn(t *testing.T) { if err != nil { t.Fatalf("CreateSession: %v", err) } - result, err := c.Prompt(ctx, sid, "no-stop-reason") + result, err := c.Prompt(ctx, sid, acp.TextPrompt("no-stop-reason")) if err != nil { t.Fatalf("Prompt: %v", err) } diff --git a/test/queue_protocol_test.go b/test/queue_protocol_test.go index 42a837e..8c35613 100644 --- a/test/queue_protocol_test.go +++ b/test/queue_protocol_test.go @@ -8,12 +8,13 @@ import ( "testing" "time" + "github.com/one710/codeye/internal/acp" "github.com/one710/codeye/internal/queue" ) type stubHandler struct{} -func (stubHandler) Prompt(_ context.Context, _, _ string) (queue.PromptResult, error) { +func (stubHandler) Prompt(_ context.Context, _ string, _ []acp.PromptPart) (queue.PromptResult, error) { return queue.PromptResult{StopReason: "end_turn"}, nil } func (stubHandler) Cancel(_ context.Context, _ string) error { return nil } @@ -24,7 +25,7 @@ func (stubHandler) SetConfigOption(_ context.Context, _, _, _ string) error { type failHandler struct{} -func (failHandler) Prompt(_ context.Context, _, _ string) (queue.PromptResult, error) { +func (failHandler) Prompt(_ context.Context, _ string, _ []acp.PromptPart) (queue.PromptResult, error) { return queue.PromptResult{}, fmt.Errorf("prompt failed") } func (failHandler) Cancel(_ context.Context, _ string) error { return fmt.Errorf("cancel failed") } @@ -179,7 +180,7 @@ func TestSetConfigFailureReturnsError(t *testing.T) { type slowHandler struct{} -func (slowHandler) Prompt(_ context.Context, _, _ string) (queue.PromptResult, error) { +func (slowHandler) Prompt(_ context.Context, _ string, _ []acp.PromptPart) (queue.PromptResult, error) { time.Sleep(2 * time.Second) return queue.PromptResult{StopReason: "end_turn"}, nil } diff --git a/test/runtime_owner_test.go b/test/runtime_owner_test.go index ab0cc3e..756396c 100644 --- a/test/runtime_owner_test.go +++ b/test/runtime_owner_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/one710/codeye/internal/acp" "github.com/one710/codeye/internal/client" "github.com/one710/codeye/internal/permissions" "github.com/one710/codeye/internal/queue" @@ -134,7 +135,7 @@ func TestEnsureWorkingSessionHealthyShortCircuit(t *testing.T) { rec := persistence.Record{RecordID: "r1", ACPSession: "s1", Agent: "test", Cwd: root} repo.Save(rec) - stopReason, err := rt.Prompt(ctx, rec, "test") + stopReason, err := rt.Prompt(ctx, rec, acp.TextPrompt("test")) if err != nil { t.Fatalf("Prompt should succeed when queue is already healthy: %v", err) } @@ -200,7 +201,7 @@ func TestRunOnceClientStartFails(t *testing.T) { rt := session.NewRuntime(repo, shortSocketForRuntime(t), 5*time.Second, 4, func() *client.Client { return client.New(client.Options{}) }) - _, err := rt.RunOnce(context.Background(), root, "hello") + _, err := rt.RunOnce(context.Background(), root, acp.TextPrompt("hello")) if err == nil { t.Fatal("expected error") } @@ -236,7 +237,7 @@ func TestPromptRespNotOK(t *testing.T) { repo := persistence.New(root) rt := session.NewRuntime(repo, socket, 5*time.Second, 4, nil) rec := persistence.Record{RecordID: "r1", ACPSession: "s1", Agent: "test", Cwd: root} - _, err := rt.Prompt(ctx, rec, "hello") + _, err := rt.Prompt(ctx, rec, acp.TextPrompt("hello")) if err == nil { t.Fatal("expected error for failed prompt") } diff --git a/test/session_runtime_test.go b/test/session_runtime_test.go index 5814785..f956eba 100644 --- a/test/session_runtime_test.go +++ b/test/session_runtime_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/one710/codeye/internal/acp" "github.com/one710/codeye/internal/client" "github.com/one710/codeye/internal/permissions" "github.com/one710/codeye/internal/queue" @@ -78,7 +79,7 @@ func TestRunOnce(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - stopReason, err := rt.RunOnce(ctx, cwd, "hello") + stopReason, err := rt.RunOnce(ctx, cwd, acp.TextPrompt("hello")) if err != nil { t.Fatalf("RunOnce: %v", err) } @@ -147,7 +148,7 @@ func TestPromptThroughQueue(t *testing.T) { rec := persistence.Record{RecordID: "r1", ACPSession: "s1", Agent: "test", Cwd: t.TempDir()} repo.Save(rec) - stopReason, err := rt.Prompt(context.Background(), rec, "hello") + stopReason, err := rt.Prompt(context.Background(), rec, acp.TextPrompt("hello")) if err != nil { t.Fatalf("Prompt: %v", err) } @@ -206,7 +207,7 @@ func TestPromptEmptyStopReasonDefault(t *testing.T) { rec := persistence.Record{RecordID: "r1", ACPSession: "s1", Agent: "test", Cwd: root} _ = repo.Save(rec) - stopReason, err := rt.Prompt(ctx, rec, "hello") + stopReason, err := rt.Prompt(ctx, rec, acp.TextPrompt("hello")) if err != nil { t.Fatalf("Prompt: %v", err) } @@ -217,7 +218,7 @@ func TestPromptEmptyStopReasonDefault(t *testing.T) { type emptyStopReasonHandler struct{} -func (emptyStopReasonHandler) Prompt(_ context.Context, _, _ string) (queue.PromptResult, error) { +func (emptyStopReasonHandler) Prompt(_ context.Context, _ string, _ []acp.PromptPart) (queue.PromptResult, error) { return queue.PromptResult{StopReason: ""}, nil } func (emptyStopReasonHandler) Cancel(_ context.Context, _ string) error { return nil }