Skip to content
Merged
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
67 changes: 67 additions & 0 deletions pkg/path/display.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package path

import (
"os"
"path/filepath"
"strings"
)

// RelativeTo returns p relative to baseDir when both paths are absolute and
// filepath.Rel can compute a relative path. Otherwise it returns p cleaned.
func RelativeTo(p, baseDir string) string {
if p == "" {
return p
}

p = filepath.Clean(p)
if baseDir == "" || !filepath.IsAbs(p) || !filepath.IsAbs(baseDir) {
return p
}

rel, err := filepath.Rel(filepath.Clean(baseDir), p)
if err != nil {
return p
}
return rel
}

// ShortenHome replaces the current user's home directory prefix with "~".
func ShortenHome(p string) string {
homeDir, err := os.UserHomeDir()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[LOW] ShortenHome calls os.UserHomeDir() on every invocation

ShortenHome calls os.UserHomeDir() directly on each call, which involves an environment variable lookup or syscall each time. The previous ShortenPath helper used the cached result from paths.GetHomeDir() (an atomic pointer). Since this function is called from multiple TUI rendering paths (directorytree, editfile, listdirectory, readfile, readmultiplefiles, searchfilescontent) which may be invoked on every render frame, this is a minor performance regression. Consider caching the home dir at package init or accepting it as a parameter (as ShortenHomeDir already does).

if err != nil || homeDir == "" {
return p
}
return ShortenHomeDir(p, homeDir)
}

// ShortenHomeDir replaces a leading homeDir prefix with "~".
func ShortenHomeDir(p, homeDir string) string {
if p == "" || homeDir == "" {
return p
}

p = filepath.Clean(p)
homeDir = filepath.Clean(homeDir)
if !IsWithin(p, homeDir) {
return p
}

rel, err := filepath.Rel(homeDir, p)
if err != nil {
return p
}
if rel == "." {
return "~"
}
return filepath.Join("~", rel)
}

// IsWithin reports whether p is equal to dir or contained by dir.
func IsWithin(p, dir string) bool {
if p == "" || dir == "" {
return false
}

rel, err := filepath.Rel(filepath.Clean(dir), filepath.Clean(p))
return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
37 changes: 37 additions & 0 deletions pkg/path/display_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package path

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func TestRelativeTo(t *testing.T) {
t.Parallel()
base := t.TempDir()

assert.Equal(t, filepath.Join("dir", "file.txt"), RelativeTo(filepath.Join(base, "dir", "file.txt"), base))
assert.Equal(t, filepath.Join("..", "sibling"), RelativeTo(filepath.Join(filepath.Dir(base), "sibling"), base))
assert.Equal(t, "relative/path", RelativeTo("relative/path", base))
assert.Equal(t, filepath.Join(base, "file.txt"), RelativeTo(filepath.Join(base, "file.txt"), "relative/base"))
}

func TestShortenHomeDir(t *testing.T) {
t.Parallel()
home := t.TempDir()

assert.Equal(t, "~", ShortenHomeDir(home, home))
assert.Equal(t, filepath.Join("~", ".agents", "skills", "commit"), ShortenHomeDir(filepath.Join(home, ".agents", "skills", "commit"), home))
assert.Equal(t, filepath.Join(filepath.Dir(home), "elsewhere"), ShortenHomeDir(filepath.Join(filepath.Dir(home), "elsewhere"), home))
}

func TestIsWithin(t *testing.T) {
t.Parallel()
base := t.TempDir()

assert.True(t, IsWithin(base, base))
assert.True(t, IsWithin(filepath.Join(base, "child"), base))
assert.False(t, IsWithin(filepath.Dir(base), base))
assert.False(t, IsWithin(filepath.Join(filepath.Dir(base), filepath.Base(base)+"-suffix"), base))
}
22 changes: 3 additions & 19 deletions pkg/sandbox/kit/kit.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
"github.com/docker/docker-agent/pkg/config"
latestcfg "github.com/docker/docker-agent/pkg/config/latest"
"github.com/docker/docker-agent/pkg/environment"
pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/paths"
"github.com/docker/docker-agent/pkg/promptfiles"
"github.com/docker/docker-agent/pkg/skills"
Expand Down Expand Up @@ -715,7 +716,7 @@ func (r *Result) PrintSummary(w io.Writer) {
fmt.Fprintln(w, " prompt files:")
for _, e := range promptEntries {
name := promptFileName(e)
notes := []string{"from " + displayHostPath(e.Source)}
notes := []string{"from " + pathx.ShortenHome(e.Source)}
if !e.IsStaged() {
notes = append(notes, "workspace mount")
} else if redacted[e.Target] {
Expand Down Expand Up @@ -781,24 +782,7 @@ func displaySkillHeader(e Entry) string {
if e.Source == "" {
return name
}
return fmt.Sprintf("%s (from %s)", name, displayHostPath(e.Source))
}

// displayHostPath replaces the user's $HOME prefix with "~" so the
// printed paths stay short and don't reveal the local username when
// shared in screenshots.
func displayHostPath(p string) string {
home, err := os.UserHomeDir()
if err != nil || home == "" {
return p
}
if strings.HasPrefix(p, home+string(filepath.Separator)) {
return "~" + p[len(home):]
}
if p == home {
return "~"
}
return p
return fmt.Sprintf("%s (from %s)", name, pathx.ShortenHome(e.Source))
}

// summaryCounts formats the trailing line of PrintSummary.
Expand Down
31 changes: 26 additions & 5 deletions pkg/skills/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"path/filepath"
"slices"

pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/paths"
)

Expand Down Expand Up @@ -51,11 +52,7 @@ func localSearchPaths() []localSearchPath {
var searchPaths []localSearchPath

if home := paths.GetHomeDir(); home != "" {
searchPaths = append(searchPaths,
localSearchPath{filepath.Join(home, ".codex", "skills"), true},
localSearchPath{filepath.Join(home, ".claude", "skills"), false},
localSearchPath{filepath.Join(home, ".agents", "skills"), true},
)
searchPaths = append(searchPaths, homeSkillSearchPaths(home)...)
}

cwd, err := os.Getwd()
Expand All @@ -80,6 +77,30 @@ func loadLocalSkillsInto(skillMap map[string]Skill) {
}
}

// IsHomeSkillPath reports whether path is under one of the global skill
// directories in the user's home directory.
func IsHomeSkillPath(path string) bool {
home := paths.GetHomeDir()
if home == "" {
return false
}

for _, p := range homeSkillSearchPaths(home) {
if pathx.IsWithin(path, p.dir) {
return true
}
}
return false
}

func homeSkillSearchPaths(home string) []localSearchPath {
return []localSearchPath{
{filepath.Join(home, ".codex", "skills"), true},
{filepath.Join(home, ".claude", "skills"), false},
{filepath.Join(home, ".agents", "skills"), true},
}
}

// projectSearchDirs returns directories from the enclosing git root down to
// cwd (inclusive), ordered from root to cwd. If cwd is not inside a git
// repository, it returns just cwd.
Expand Down
12 changes: 12 additions & 0 deletions pkg/skills/skills_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,18 @@ func TestLoad_KitDirOverridesEverything(t *testing.T) {
assert.NotContains(t, names, "host-only", "host paths must be ignored when a kit is set")
}

func TestIsHomeSkillPath(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)

assert.True(t, IsHomeSkillPath(filepath.Join(home, ".codex", "skills", "skill-a")))
assert.True(t, IsHomeSkillPath(filepath.Join(home, ".claude", "skills", "skill-b")))
assert.True(t, IsHomeSkillPath(filepath.Join(home, ".agents", "skills", "skill-c")))
assert.False(t, IsHomeSkillPath(filepath.Join(home, "work", "repo", ".agents", "skills", "project-skill")))
assert.False(t, IsHomeSkillPath(filepath.Join(filepath.Dir(home), "elsewhere", "skill")))
}

func TestSkill_IsFork(t *testing.T) {
assert.True(t, (&Skill{Context: "fork"}).IsFork())
assert.False(t, (&Skill{Context: ""}).IsFork())
Expand Down
3 changes: 2 additions & 1 deletion pkg/tui/components/tool/directorytree/directorytree.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package directorytree
import (
"strings"

pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem"
"github.com/docker/docker-agent/pkg/tui/components/toolcommon"
"github.com/docker/docker-agent/pkg/tui/core/layout"
Expand All @@ -12,7 +13,7 @@ import (

func New(msg *types.Message, sessionState service.SessionStateReader) layout.Model {
return toolcommon.NewBase(msg, sessionState, toolcommon.SimpleRendererWithResult(
toolcommon.ExtractField(func(a filesystem.DirectoryTreeArgs) string { return toolcommon.ShortenPath(a.Path) }),
toolcommon.ExtractField(func(a filesystem.DirectoryTreeArgs) string { return pathx.ShortenHome(a.Path) }),
extractResult,
))
}
Expand Down
5 changes: 3 additions & 2 deletions pkg/tui/components/tool/editfile/editfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package editfile
import (
"fmt"

pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem"
"github.com/docker/docker-agent/pkg/tui/components/spinner"
"github.com/docker/docker-agent/pkg/tui/components/toolcommon"
Expand Down Expand Up @@ -72,7 +73,7 @@ func render(
"%s%s %s",
toolcommon.Icon(msg, s),
styles.ToolName.Render(msg.ToolDefinition.DisplayName()),
styles.ToolMessageStyle.Render(toolcommon.ShortenPath(args.Path)),
styles.ToolMessageStyle.Render(pathx.ShortenHome(args.Path)),
)
}

Expand Down Expand Up @@ -142,7 +143,7 @@ func renderCollapsed(
"%s%s %s%s",
toolcommon.Icon(msg, s),
styles.ToolName.Render(msg.ToolDefinition.DisplayName()),
styles.ToolMessageStyle.Render(toolcommon.ShortenPath(args.Path)),
styles.ToolMessageStyle.Render(pathx.ShortenHome(args.Path)),
diffSummary,
)

Expand Down
3 changes: 2 additions & 1 deletion pkg/tui/components/tool/listdirectory/listdirectory.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package listdirectory
import (
"strings"

pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem"
"github.com/docker/docker-agent/pkg/tui/components/toolcommon"
"github.com/docker/docker-agent/pkg/tui/core/layout"
Expand All @@ -12,7 +13,7 @@ import (

func New(msg *types.Message, sessionState service.SessionStateReader) layout.Model {
return toolcommon.NewBase(msg, sessionState, toolcommon.SimpleRendererWithResult(
toolcommon.ExtractField(func(a filesystem.ListDirectoryArgs) string { return toolcommon.ShortenPath(a.Path) }),
toolcommon.ExtractField(func(a filesystem.ListDirectoryArgs) string { return pathx.ShortenHome(a.Path) }),
extractResult,
))
}
Expand Down
39 changes: 0 additions & 39 deletions pkg/tui/components/tool/listdirectory/listdirectory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (

"github.com/docker/docker-agent/pkg/tools"
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem"
"github.com/docker/docker-agent/pkg/tui/components/toolcommon"
"github.com/docker/docker-agent/pkg/tui/types"
)

Expand Down Expand Up @@ -70,41 +69,3 @@ func TestExtractResult(t *testing.T) {
})
}
}

func TestShortenPath(t *testing.T) {
tests := []struct {
name string
path string
expected string
}{
{
name: "empty path",
path: "",
expected: "",
},
{
name: "current directory",
path: ".",
expected: ".",
},
{
name: "absolute path",
path: "/usr/local/bin",
expected: "/usr/local/bin",
},
{
name: "relative path",
path: "src/components",
expected: "src/components",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := toolcommon.ShortenPath(tt.path)
if result != tt.expected {
t.Errorf("ShortenPath(%q) = %q, want %q", tt.path, result, tt.expected)
}
})
}
}
3 changes: 2 additions & 1 deletion pkg/tui/components/tool/readfile/readfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package readfile
import (
"fmt"

pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem"
"github.com/docker/docker-agent/pkg/tui/components/toolcommon"
"github.com/docker/docker-agent/pkg/tui/core/layout"
Expand All @@ -12,7 +13,7 @@ import (

func New(msg *types.Message, sessionState service.SessionStateReader) layout.Model {
return toolcommon.NewBase(msg, sessionState, toolcommon.SimpleRendererWithResult(
toolcommon.ExtractField(func(a filesystem.ReadFileArgs) string { return toolcommon.ShortenPath(a.Path) }),
toolcommon.ExtractField(func(a filesystem.ReadFileArgs) string { return pathx.ShortenHome(a.Path) }),
extractResult,
))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"charm.land/lipgloss/v2"

pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem"
"github.com/docker/docker-agent/pkg/tui/components/spinner"
"github.com/docker/docker-agent/pkg/tui/components/toolcommon"
Expand Down Expand Up @@ -94,7 +95,7 @@ func formatSummaryLines(meta *filesystem.ReadMultipleFilesMeta) []fileSummary {

var summaries []fileSummary
for _, file := range meta.Files {
path := toolcommon.ShortenPath(file.Path)
path := pathx.ShortenHome(file.Path)
var output string
if file.Error != "" {
output = " " + file.Error
Expand All @@ -120,7 +121,7 @@ func formatFilesList(filePaths []string) string {

shortened := make([]string, len(filePaths))
for i, p := range filePaths {
shortened[i] = toolcommon.ShortenPath(p)
shortened[i] = pathx.ShortenHome(p)
}

if len(shortened) == 1 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package searchfilescontent
import (
"fmt"

pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem"
"github.com/docker/docker-agent/pkg/tui/components/toolcommon"
"github.com/docker/docker-agent/pkg/tui/core/layout"
Expand All @@ -23,7 +24,7 @@ func extractArgs(args string) string {
return ""
}

path := toolcommon.ShortenPath(parsed.Path)
path := pathx.ShortenHome(parsed.Path)
query := parsed.Query
if len(query) > 30 {
query = query[:27] + "..."
Expand Down
Loading
Loading