forked from bots-garden/ori
| ✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS | 1 | package agent |
| 2 | ||
| 3 | import ( | |
| 4 | "context" | |
| 5 | "fmt" | |
| 6 | "os" | |
| 7 | "path/filepath" | |
| 8 | "strings" | |
| 9 | ||
| 10 | acp "github.com/coder/acp-go-sdk" | |
| 11 | ) | |
| 12 | ||
| 13 | // client implements acp.Client: the callbacks the agent invokes on us. | |
| 14 | // Session updates and permission requests are delegated to the Handler; file | |
| 15 | // system access is served directly; terminals are not supported in this | |
| 16 | // version, matching the capabilities announced during initialize. | |
| 17 | type client struct { | |
| 18 | handler Handler | |
| 19 | } | |
| 20 | ||
| 21 | var _ acp.Client = (*client)(nil) | |
| 22 | ||
| 23 | func (c *client) SessionUpdate(ctx context.Context, params acp.SessionNotification) error { | |
| 24 | c.handler.HandleSessionUpdate(ctx, params) | |
| 25 | return nil | |
| 26 | } | |
| 27 | ||
| 28 | func (c *client) RequestPermission(ctx context.Context, params acp.RequestPermissionRequest) (acp.RequestPermissionResponse, error) { | |
| 29 | return c.handler.HandlePermissionRequest(ctx, params) | |
| 30 | } | |
| 31 | ||
| 32 | func (c *client) ReadTextFile(_ context.Context, params acp.ReadTextFileRequest) (acp.ReadTextFileResponse, error) { | |
| 33 | if !filepath.IsAbs(params.Path) { | |
| 34 | return acp.ReadTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path) | |
| 35 | } | |
| 36 | raw, err := os.ReadFile(params.Path) | |
| 37 | if err != nil { | |
| 38 | return acp.ReadTextFileResponse{}, fmt.Errorf("read %s: %w", params.Path, err) | |
| 39 | } | |
| 40 | return acp.ReadTextFileResponse{Content: sliceLines(string(raw), params.Line, params.Limit)}, nil | |
| 41 | } | |
| 42 | ||
| 43 | func (c *client) WriteTextFile(_ context.Context, params acp.WriteTextFileRequest) (acp.WriteTextFileResponse, error) { | |
| 44 | if !filepath.IsAbs(params.Path) { | |
| 45 | return acp.WriteTextFileResponse{}, fmt.Errorf("path must be absolute: %s", params.Path) | |
| 46 | } | |
| 47 | if dir := filepath.Dir(params.Path); dir != "" { | |
| 48 | if err := os.MkdirAll(dir, 0o755); err != nil { | |
| 49 | return acp.WriteTextFileResponse{}, fmt.Errorf("mkdir %s: %w", dir, err) | |
| 50 | } | |
| 51 | } | |
| 52 | if err := os.WriteFile(params.Path, []byte(params.Content), 0o644); err != nil { | |
| 53 | return acp.WriteTextFileResponse{}, fmt.Errorf("write %s: %w", params.Path, err) | |
| 54 | } | |
| 55 | return acp.WriteTextFileResponse{}, nil | |
| 56 | } | |
| 57 | ||
| 58 | // sliceLines applies ACP's optional 1-based line offset and line count to a | |
| 59 | // file's content. | |
| 60 | func sliceLines(content string, line, limit *int) string { | |
| 61 | if line == nil && limit == nil { | |
| 62 | return content | |
| 63 | } | |
| 64 | lines := strings.Split(content, "\n") | |
| 65 | start := 0 | |
| 66 | if line != nil && *line > 0 { | |
| 67 | start = min(*line-1, len(lines)) | |
| 68 | } | |
| 69 | end := len(lines) | |
| 70 | if limit != nil && *limit > 0 && start+*limit < end { | |
| 71 | end = start + *limit | |
| 72 | } | |
| 73 | return strings.Join(lines[start:end], "\n") | |
| 74 | } | |
| 75 | ||
| 76 | // Terminal support is announced as absent during initialize; a conforming | |
| 77 | // agent never calls these. They still answer with a proper JSON-RPC error | |
| 78 | // instead of panicking on a non-conforming one. | |
| 79 | ||
| 80 | func (c *client) CreateTerminal(context.Context, acp.CreateTerminalRequest) (acp.CreateTerminalResponse, error) { | |
| 81 | return acp.CreateTerminalResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalCreate) | |
| 82 | } | |
| 83 | ||
| 84 | func (c *client) KillTerminal(context.Context, acp.KillTerminalRequest) (acp.KillTerminalResponse, error) { | |
| 85 | return acp.KillTerminalResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalKill) | |
| 86 | } | |
| 87 | ||
| 88 | func (c *client) TerminalOutput(context.Context, acp.TerminalOutputRequest) (acp.TerminalOutputResponse, error) { | |
| 89 | return acp.TerminalOutputResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalOutput) | |
| 90 | } | |
| 91 | ||
| 92 | func (c *client) ReleaseTerminal(context.Context, acp.ReleaseTerminalRequest) (acp.ReleaseTerminalResponse, error) { | |
| 93 | return acp.ReleaseTerminalResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalRelease) | |
| 94 | } | |
| 95 | ||
| 96 | func (c *client) WaitForTerminalExit(context.Context, acp.WaitForTerminalExitRequest) (acp.WaitForTerminalExitResponse, error) { | |
| 97 | return acp.WaitForTerminalExitResponse{}, acp.NewMethodNotFound(acp.ClientMethodTerminalWaitForExit) | |
| 98 | } |