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
6 changes: 5 additions & 1 deletion .claude/skills/codemap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ Skills matched: hub-safety, refactor - run `codemap skill show <name>` for guida

### Tree View (`codemap .`)
- `⭐️ name ←N` marks a hub: N files import it (a Go package is marked on its directory); the legend under the tree says so
- Directories flattened when containing single subdirectory
- `hooks.go 2535L` is the file's line count, shown at 100+ lines; read a range instead of the whole file
- `x.go (+3 tests)` folds same-directory tests into their source; `+N other tests` counts tests with no source beside them (a `tests/` dir lists its files)
- `page-*.yml ×9` collapses 4+ siblings that differ only in numbers or hashes; `dir/ (untracked, N files, SIZE)` is a wholly untracked directory
- Directories flattened when containing single subdirectory; a directory holding one file prints as `dir/FILE`
- After the tree: the legend, then `Hubs (top 10 of N, by importers):` with one hub per line (`←73 scanner/`, full path, line count when 100+), then the next commands

### Dependency Flow (`codemap --deps`)
- External dependencies grouped by language
Expand Down
41 changes: 33 additions & 8 deletions cmd/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,9 +503,10 @@ func hookSessionStart(root string) error {
depth = projCfg.Depth
}
exe, exeErr := os.Executable()
rendered := map[int]string{}
renderTree := func(depth int) string {
if out, ok := rendered[depth]; ok {
rendered := map[string]string{}
renderTreeAt := func(depth int, lean string) string {
key := strconv.Itoa(depth) + "/" + lean
if out, ok := rendered[key]; ok {
return out
}
if exeErr != nil {
Expand All @@ -520,26 +521,39 @@ func hookSessionStart(root string) error {
}
args = append(args, root)
cmd := exec.Command(exe, args...)
if lean != "" {
cmd.Env = append(os.Environ(), TreeLeanEnv+"="+lean)
}
var buf strings.Builder
cmd.Stdout = &buf
cmd.Stderr = os.Stderr
cmd.Run()
rendered[depth] = buf.String() + "\n"
return rendered[depth]
rendered[key] = buf.String() + "\n"
return rendered[key]
}

// A tree with its own hub legend already marks every hub with its count;
// the hook's hub list would state them twice.
if !treeStatesHubs(renderTree(depth)) {
if !treeStatesHubs(renderTreeAt(depth, "")) {
trailer = hubBlock + trailer
}
// Spend the budget on file names first: shed the tree's extras at full
// depth before fitSessionStart gives up depth.
maxBytes := sessionStartOutputCap()
treeBudget := min(structureBudget, maxBytes-len(header)-len(cutAtLine(trailer, maxBytes/2, "... (truncated to fit the hook output cap)\n")))
lean := pickTreeLean(func(level string) string { return renderTreeAt(depth, level) }, treeBudget)
renderTree := func(depth int) string { return renderTreeAt(depth, lean) }

fmt.Print(fitSessionStart(header, trailer, depth, structureBudget, sessionStartOutputCap(), renderTree))
fmt.Print(fitSessionStart(header, trailer, depth, structureBudget, maxBytes, renderTree))

return nil
}

// Markers of the tree's hub legend (render.printHubLegend).
// TreeLeanEnv tells a tree render to shed extras: "1" cuts the hub list to
// three, "2" also drops line counts.
const TreeLeanEnv = "CODEMAP_TREE_LEAN"

// Markers of the tree's own hub trailer (render/tree_compact.go).
const (
treeHubLegend = "⭐️ ←N = "
treeNoHubLegend = "No ⭐️ hub marks:"
Expand All @@ -549,6 +563,17 @@ func treeStatesHubs(tree string) bool {
return strings.Contains(tree, treeHubLegend) || strings.Contains(tree, treeNoHubLegend)
}

// pickTreeLean returns the richest lean level whose render fits budget, or
// the leanest when none does (depth is cut after that).
func pickTreeLean(render func(lean string) string, budget int) string {
for _, level := range []string{"", "1"} {
if len(render(level)) <= budget {
return level
}
}
return "2"
}

// sessionStartOutputCap is the whole session-start budget; CODEMAP_HOOK_OUTPUT_CAP
// overrides it for hosts with a different limit.
func sessionStartOutputCap() int {
Expand Down
16 changes: 16 additions & 0 deletions cmd/hooks_tree_lean_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package cmd

import (
"strings"
"testing"
)

func TestPickTreeLeanShedsExtrasBeforeDepth(t *testing.T) {
sizes := map[string]int{"": 120, "1": 90, "2": 60}
render := func(lean string) string { return strings.Repeat("x", sizes[lean]) }
for budget, want := range map[int]string{200: "", 100: "1", 70: "2", 10: "2"} {
if got := pickTreeLean(render, budget); got != want {
t.Errorf("budget %d: lean %q, want %q", budget, got, want)
}
}
}
2 changes: 1 addition & 1 deletion docs/MCP.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ and managed MCP definitions created while it is active retain it automatically.

| Tool | Description |
|------|-------------|
| `get_structure` | Project tree view with file sizes and language detection |
| `get_structure` | Project tree view with file sizes and language detection, tests folded into their source, plus the hub list |
| `get_dependencies` | Dependency flow with imports, functions, and hub files |
| `get_diff` | Changed files with line counts and impact analysis |
| `find_file` | Find files by name pattern |
Expand Down
8 changes: 8 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,14 @@ func main() {
} else {
if diffInfo == nil {
project.Hubs, project.HubNote = treeHubs(absRoot, len(files), filters)
project.UntrackedDirs = untrackedDirs(root)
lean := os.Getenv(cmd.TreeLeanEnv)
if lean != "2" {
project.LineCounts = countLines(absRoot, files)
}
if lean != "" {
project.HubListMax = 3
}
}
render.Tree(os.Stdout, project)
}
Expand Down
2 changes: 1 addition & 1 deletion mcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ func NewServer(options RuntimeOptions) *mcp.Server {
// Tool: get_structure - Get project tree view
mcp.AddTool(server, &mcp.Tool{
Name: "get_structure",
Description: "Get the project structure as a tree view. Shows files organized by directory with language detection, and file sizes. Use this to understand how a codebase is organized.",
Description: "Get the project structure as a tree view. Shows files organized by directory with language detection and file sizes; tests are folded into their same-directory source file and numbered sibling runs collapse to one pattern line. Hub counts are in the HUB FILES list below the tree. Use this to understand how a codebase is organized.",
OutputSchema: mustSchemaFor[StructureOutput](),
}, handleGetStructure)

Expand Down
3 changes: 3 additions & 0 deletions mcp/parity_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ func TestStructureDepthPrecedence(t *testing.T) {
for path, body := range map[string]string{
"a/one/b/c.go": "package b\n",
"a/two/d/e.go": "package d\n",
// A second file keeps a/two/ from being a one-line single-file chain,
// which the tree prints whatever the depth.
"a/two/d/f.go": "package d\n",
} {
full := filepath.Join(root, path)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
Expand Down
6 changes: 5 additions & 1 deletion plugins/codemap/skills/codemap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ Skills matched: hub-safety, refactor - run `codemap skill show <name>` for guida

### Tree View (`codemap .`)
- `⭐️ name ←N` marks a hub: N files import it (a Go package is marked on its directory); the legend under the tree says so
- Directories flattened when containing single subdirectory
- `hooks.go 2535L` is the file's line count, shown at 100+ lines; read a range instead of the whole file
- `x.go (+3 tests)` folds same-directory tests into their source; `+N other tests` counts tests with no source beside them (a `tests/` dir lists its files)
- `page-*.yml ×9` collapses 4+ siblings that differ only in numbers or hashes; `dir/ (untracked, N files, SIZE)` is a wholly untracked directory
- Directories flattened when containing single subdirectory; a directory holding one file prints as `dir/FILE`
- After the tree: the legend, then `Hubs (top 10 of N, by importers):` with one hub per line (`←73 scanner/`, full path, line count when 100+), then the next commands

### Dependency Flow (`codemap --deps`)
- External dependencies grouped by language
Expand Down
Loading
Loading