Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cmd/cu/checkout.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
)

var checkoutCmd = &cobra.Command{
Use: "checkout <env>",
Short: "Switch to an environment's branch locally",
Use: "checkout <env>",
Short: "Switch to an environment's branch locally",
Long: `Bring an environment's work into your local git workspace.
This creates a local branch from the environment's state so you can
explore files in your IDE, make changes, or continue development.`,
Expand Down Expand Up @@ -48,4 +48,4 @@ cu checkout fancy-mallard -b my-review-branch`,
func init() {
checkoutCmd.Flags().StringP("branch", "b", "", "Local branch name to use")
rootCmd.AddCommand(checkoutCmd)
}
}
6 changes: 3 additions & 3 deletions cmd/cu/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
)

var deleteCmd = &cobra.Command{
Use: "delete [<env>...]",
Short: "Delete environments and start fresh",
Use: "delete [<env>...]",
Short: "Delete environments and start fresh",
Long: `Delete one or more environments and their associated resources.
This permanently removes the environment's branch and container state.
Use this when starting over with a different approach.
Expand Down Expand Up @@ -80,4 +80,4 @@ cu delete --all`,
func init() {
rootCmd.AddCommand(deleteCmd)
deleteCmd.Flags().Bool("all", false, "Delete all environments")
}
}
19 changes: 13 additions & 6 deletions cmd/cu/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@ import (
)

var diffCmd = &cobra.Command{
Use: "diff <env>",
Short: "Show what files an agent changed",
Use: "diff <env>",
Short: "Show what files an agent changed",
Long: `Display the code changes made by an agent in an environment.
Shows a git diff between the environment's state and your current branch.`,
Shows a git diff of all changes made since the environment was created.
Use -b to compare against a specific branch instead of showing full diff.`,
Args: cobra.ExactArgs(1),
ValidArgsFunction: suggestEnvironments,
Example: `# See what changes the agent made
Example: `# See what changes the agent made (full diff)
cu diff fancy-mallard

# Compare against main branch
cu diff fancy-mallard -b main

# Quick assessment before merging
cu diff backend-api`,
cu diff backend-api -b main`,
Copy link
Contributor

@cwlbraa cwlbraa Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in raw git, isn't this equivalent to git diff main fancy-mallard? or git diff fancy-mallard main we should make sure we're copying that UX wherever possible.

also why does it say backend-api, those are adverb animals lol

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cu diff is slightly different than a git diff ... out of the box, it does a git diff $(git merge-base $(git branch --show-current) CU_ENV))..CU_ENV

The -b here is replacing the merge-base stuff with whatever is in -b

Really unsure whether we should copy the syntax or not here, especially since I think we'll want to support paths (like git) and the syntax gets tricky

git diff main fancy-mallard
git diff main..fancy-mallard
git diff main..fancy-mallard ./cmd
git diff main fancy-mallard -- ./cmd

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any opinion from a git UX POV?

RunE: func(app *cobra.Command, args []string) error {
ctx := app.Context()

Expand All @@ -28,10 +32,13 @@ cu diff backend-api`,
return err
}

return repo.Diff(ctx, args[0], os.Stdout)
branch, _ := app.Flags().GetString("branch")

return repo.Diff(ctx, args[0], branch, os.Stdout)
},
}

func init() {
diffCmd.Flags().StringP("branch", "b", "", "Compare against specified branch (uses merge-base)")
rootCmd.AddCommand(diffCmd)
}
2 changes: 1 addition & 1 deletion cmd/cu/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ func init() {
listCmd.Flags().BoolP("quiet", "q", false, "Display only environment IDs")
listCmd.Flags().BoolP("no-trunc", "", false, "Don't truncate output")
rootCmd.AddCommand(listCmd)
}
}
21 changes: 15 additions & 6 deletions cmd/cu/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,25 @@ import (
)

var logCmd = &cobra.Command{
Use: "log <env>",
Short: "View what an agent did step-by-step",
Use: "log <env>",
Short: "View what an agent did step-by-step",
Long: `Display the complete development history for an environment.
Shows all commits made by the agent plus command execution notes.
Use -p to include code patches in the output.`,
Use -p to include code patches in the output.
Use -b to compare against a specific branch instead of showing full history.`,
Args: cobra.ExactArgs(1),
ValidArgsFunction: suggestEnvironments,
Example: `# See what agent did
Example: `# See what agent did (full history)
cu log fancy-mallard

# Include code changes
cu log fancy-mallard -p`,
cu log fancy-mallard -p

# Compare against main branch
cu log fancy-mallard -b main

# Compare against main with patches
cu log fancy-mallard -b main -p`,
RunE: func(app *cobra.Command, args []string) error {
ctx := app.Context()

Expand All @@ -30,12 +37,14 @@ cu log fancy-mallard -p`,
}

patch, _ := app.Flags().GetBool("patch")
branch, _ := app.Flags().GetString("branch")

return repo.Log(ctx, args[0], patch, os.Stdout)
return repo.Log(ctx, args[0], patch, branch, os.Stdout)
},
}

func init() {
logCmd.Flags().BoolP("patch", "p", false, "Generate patch")
logCmd.Flags().StringP("branch", "b", "", "Compare against specified branch (uses merge-base)")
rootCmd.AddCommand(logCmd)
}
2 changes: 1 addition & 1 deletion cmd/cu/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ cu watch`,

func init() {
rootCmd.AddCommand(watchCmd)
}
}
10 changes: 5 additions & 5 deletions environment/integration/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func TestRepositoryLog(t *testing.T) {

// Get commit log without patches
var logBuf bytes.Buffer
err := repo.Log(ctx, env.ID, false, &logBuf)
err := repo.Log(ctx, env.ID, false, "", &logBuf)
logOutput := logBuf.String()
require.NoError(t, err, logOutput)

Expand All @@ -153,7 +153,7 @@ func TestRepositoryLog(t *testing.T) {

// Get commit log with patches
logBuf.Reset()
err = repo.Log(ctx, env.ID, true, &logBuf)
err = repo.Log(ctx, env.ID, true, "", &logBuf)
logWithPatchOutput := logBuf.String()
require.NoError(t, err, logWithPatchOutput)

Expand All @@ -162,7 +162,7 @@ func TestRepositoryLog(t *testing.T) {
assert.Contains(t, logWithPatchOutput, "+updated content")

// Test log for non-existent environment
err = repo.Log(ctx, "non-existent-env", false, &logBuf)
err = repo.Log(ctx, "non-existent-env", false, "", &logBuf)
assert.Error(t, err)
})
}
Expand All @@ -184,15 +184,15 @@ func TestRepositoryDiff(t *testing.T) {

// Get diff output
var diffBuf bytes.Buffer
err := repo.Diff(ctx, env.ID, &diffBuf)
err := repo.Diff(ctx, env.ID, "", &diffBuf)
diffOutput := diffBuf.String()
require.NoError(t, err, diffOutput)

// Verify diff contains expected changes
assert.Contains(t, diffOutput, "+updated content")

// Test diff with non-existent environment
err = repo.Diff(ctx, "non-existent-env", &diffBuf)
err = repo.Diff(ctx, "non-existent-env", "", &diffBuf)
assert.Error(t, err)
})
}
80 changes: 80 additions & 0 deletions mcpserver/tools.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mcpserver

import (
"bytes"
"context"
_ "embed"
"encoding/json"
Expand Down Expand Up @@ -115,6 +116,8 @@ func init() {
EnvironmentAddServiceTool,

EnvironmentCheckpointTool,
EnvironmentLogTool,
EnvironmentDiffTool,
)
}

Expand Down Expand Up @@ -816,3 +819,80 @@ Supported schemas are:
return mcp.NewToolResultText(fmt.Sprintf("Service added and started successfully: %s", string(output))), nil
},
}

var EnvironmentLogTool = &Tool{
Definition: mcp.NewTool("environment_log",
mcp.WithDescription("View the development history of an environment, showing all commits made by the agent plus command execution notes."),
mcp.WithString("explanation",
mcp.Description("One sentence explanation for why this environment log is being viewed."),
),
mcp.WithString("environment_source",
mcp.Description("Absolute path to the source git repository for the environment."),
mcp.Required(),
),
mcp.WithString("environment_id",
mcp.Description("The ID of the environment to view the log for."),
mcp.Required(),
),
mcp.WithBoolean("patch",
mcp.Description("Include code patches in the output (default: false)."),
),
),
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
repo, err := openRepository(ctx, request)
if err != nil {
return mcp.NewToolResultErrorFromErr("unable to open the repository", err), nil
}

envID, err := request.RequireString("environment_id")
if err != nil {
return nil, err
}

patch := request.GetBool("patch", false)

var buf bytes.Buffer
// MCP tools always show full history (no branch comparison)
if err := repo.Log(ctx, envID, patch, "", &buf); err != nil {
return mcp.NewToolResultErrorFromErr("failed to get environment log", err), nil
}

return mcp.NewToolResultText(buf.String()), nil
},
}

var EnvironmentDiffTool = &Tool{
Definition: mcp.NewTool("environment_diff",
mcp.WithDescription("View the cumulative changes made in an environment from its creation point, showing all code modifications as a unified diff."),
mcp.WithString("explanation",
mcp.Description("One sentence explanation for why this environment diff is being viewed."),
),
mcp.WithString("environment_source",
mcp.Description("Absolute path to the source git repository for the environment."),
mcp.Required(),
),
mcp.WithString("environment_id",
mcp.Description("The ID of the environment to view the diff for."),
mcp.Required(),
),
),
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
repo, err := openRepository(ctx, request)
if err != nil {
return mcp.NewToolResultErrorFromErr("unable to open the repository", err), nil
}

envID, err := request.RequireString("environment_id")
if err != nil {
return nil, err
}

var buf bytes.Buffer
// MCP tools always show full diff (no branch comparison)
if err := repo.Diff(ctx, envID, "", &buf); err != nil {
return mcp.NewToolResultErrorFromErr("failed to get environment diff", err), nil
}

return mcp.NewToolResultText(buf.String()), nil
},
}
37 changes: 26 additions & 11 deletions repository/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,26 +239,41 @@ func (r *Repository) currentUserBranch(ctx context.Context) (string, error) {
return RunGitCommand(ctx, r.userRepoPath, "branch", "--show-current")
}

func (r *Repository) mergeBase(ctx context.Context, env *environment.EnvironmentInfo) (string, error) {
currentBranch, err := r.currentUserBranch(ctx)
if err != nil {
return "", err
}
currentBranch = strings.TrimSpace(currentBranch)
func (r *Repository) mergeBase(ctx context.Context, env *environment.EnvironmentInfo, branch string) (string, error) {
envGitRef := fmt.Sprintf("%s/%s", containerUseRemote, env.ID)
mergeBase, err := RunGitCommand(ctx, r.userRepoPath, "merge-base", currentBranch, envGitRef)
mergeBase, err := RunGitCommand(ctx, r.userRepoPath, "merge-base", branch, envGitRef)
if err != nil {
return "", err
}
return strings.TrimSpace(mergeBase), nil
}

func (r *Repository) revisionRange(ctx context.Context, env *environment.EnvironmentInfo) (string, error) {
mergeBase, err := r.mergeBase(ctx, env)
// revisionRange determines the git revision range for log/diff operations.
// If branch is provided, uses merge-base with that branch.
// Otherwise, uses merge-base with the current user branch.
func (r *Repository) revisionRange(ctx context.Context, env *environment.EnvironmentInfo, branch string) (string, error) {
envGitRef := fmt.Sprintf("%s/%s", containerUseRemote, env.ID)

if branch != "" {
// Use merge-base with specified branch
mergeBase, err := r.mergeBase(ctx, env, branch)
if err != nil {
return "", err
}
return fmt.Sprintf("%s..%s", mergeBase, envGitRef), nil
}

// Use merge-base with current user branch (original behavior)
currentBranch, err := r.currentUserBranch(ctx)
if err != nil {
return "", err
}
currentBranch = strings.TrimSpace(currentBranch)

mergeBase, err := r.mergeBase(ctx, env, currentBranch)
if err != nil {
return "", err
}
envGitRef := fmt.Sprintf("%s/%s", containerUseRemote, env.ID)
return fmt.Sprintf("%s..%s", mergeBase, envGitRef), nil
}

Expand Down Expand Up @@ -582,4 +597,4 @@ func matchesScpLike(url string) bool {
func findScpLikeComponents(url string) (user, host, port, path string) {
m := scpLikeURLRegExp.FindStringSubmatch(url)
return m[1], m[2], m[3], m[4]
}
}
16 changes: 11 additions & 5 deletions repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,10 @@ func (r *Repository) Checkout(ctx context.Context, id, branch string) (string, e
return branch, err
}

func (r *Repository) Log(ctx context.Context, id string, patch bool, w io.Writer) error {
// Log displays the development history for an environment.
// If branch is provided, shows log since merge-base with that branch.
// Otherwise, shows log from environment creation.
func (r *Repository) Log(ctx context.Context, id string, patch bool, branch string, w io.Writer) error {
envInfo, err := r.Info(ctx, id)
if err != nil {
return err
Expand All @@ -375,7 +378,7 @@ func (r *Repository) Log(ctx context.Context, id string, patch bool, w io.Writer
logArgs = append(logArgs, "--format=%C(yellow)%h%Creset %s %Cgreen(%cr)%Creset %+N")
}

revisionRange, err := r.revisionRange(ctx, envInfo)
revisionRange, err := r.revisionRange(ctx, envInfo, branch)
if err != nil {
return err
}
Expand All @@ -391,7 +394,10 @@ func (r *Repository) Log(ctx context.Context, id string, patch bool, w io.Writer
return cmd.Run()
}

func (r *Repository) Diff(ctx context.Context, id string, w io.Writer) error {
// Diff shows changes made in an environment.
// If branch is provided, shows diff since merge-base with that branch.
// Otherwise, shows diff from environment creation.
func (r *Repository) Diff(ctx context.Context, id string, branch string, w io.Writer) error {
envInfo, err := r.Info(ctx, id)
if err != nil {
return err
Expand All @@ -402,7 +408,7 @@ func (r *Repository) Diff(ctx context.Context, id string, w io.Writer) error {
"diff",
}

revisionRange, err := r.revisionRange(ctx, envInfo)
revisionRange, err := r.revisionRange(ctx, envInfo, branch)
if err != nil {
return err
}
Expand All @@ -416,4 +422,4 @@ func (r *Repository) Diff(ctx context.Context, id string, w io.Writer) error {
cmd.Stderr = w

return cmd.Run()
}
}
Loading