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
32 changes: 29 additions & 3 deletions UserGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,37 @@ These tests cover the main input choices, the connection between the command, qu

## Cathy - Repository Learning Map (`/group`)

In an OpenCode repository session, select **Learn** and run `/group`. Open a functionality group to browse its subgroups and files (18 per page), or select **EXPLAIN THIS FUNCTIONALITY** / **EXPLAIN THIS SUBGROUP** for file-based guidance, relationships, a reading order, and a learning question. If Learn has read a file in this session, rerun `/group` to prioritize its group. Structural names remain if model naming fails; inventories over 10,000 files may be partial.
The `/group` command helps students explore an unfamiliar repository through functionality groups, subgroups, and file lists. It prioritizes groups containing files read in the current session and can explain their responsibilities and relationships.

**Manual end-to-end check:** Browse a group and subgroup, change pages, and request both explanations. Check that the answers cite real files and that a newly read file moves its group up. An empty repository should show a message without crashing. This checks the live model response.
### How to use it

Open a repository session, select **Learn**, and run `/group`. Select a group and subgroup to browse files (18 per page), or choose **EXPLAIN THIS FUNCTIONALITY** / **EXPLAIN THIS SUBGROUP** for guidance, a reading order, and a learning question.

Rerun `/group` after Learn reads files to refresh prioritization. Structural names remain if model naming fails; inventories over 10,000 files may be partial.

### User testing

1. In an active Learn session, run `/group` and confirm the Repository Learning Map appears.
2. Open a group and subgroup; check file paths, pagination, and back navigation.
3. Request both explanation types. Verify cited files and relationships, a reading order, and a learning question.
4. Ask Learn to read a file, then rerun `/group`. Groups with more distinct session-read files should rank higher.
5. Try an empty repository; confirm a message appears without crashing.

### Automated tests

- [Grouping tests](packages/tui/test/util/repository-functionality.test.ts) cover layouts, limits, file preservation, prioritization, prompts, and naming fallback.
- [Read-collection tests](packages/tui/test/util/referenced-file.test.ts) cover completed reads, path normalization, deduplication, and command registration.
- [Integration tests](packages/tui/test/integration/repository-map.test.ts) trace session reads through grouping and explanation prompts.
- [TUI end-to-end tests](packages/tui/test/group-map.e2e.test.ts) use a mock server to verify navigation, both explanation requests through Learn, and empty repositories.

Run from `packages/tui`:

```bash
bun test test/util/repository-functionality.test.ts test/util/referenced-file.test.ts test/integration/repository-map.test.ts test/group-map.e2e.test.ts
bun typecheck
```

**Automated tests:** [Unit grouping](packages/tui/test/util/repository-functionality.test.ts) covers layouts, limits, prioritization, prompts, and fallback; [unit read collection](packages/tui/test/util/referenced-file.test.ts) covers completed reads, paths, and `/group` registration. The [integration test](packages/tui/test/integration/repository-map.test.ts) traces a read through grouping and explanation prompts. The [TUI E2E test](packages/tui/test/group-map.e2e.test.ts) uses a mock server to open `/group`, navigate files, and submit both explanation prompts in Learn. Together these cover #12's acceptance criteria; the manual check covers live model output. From `packages/tui`, run `bun test` and `bun typecheck`.
These tests cover the grouping and explanation workflow, including boundary and fallback cases. Manual checks verify live model answers, which the mock-server tests cannot assess.


## Janna - Code Explanations and Suggestions
Expand Down
11 changes: 10 additions & 1 deletion packages/tui/src/util/repository-functionality.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ function rank<T extends LearningArea>(areas: T[]) {
)
}

function commonDirectory(files: readonly string[]) {
const directories = files.map((file) => file.split("/").slice(0, -1))
const first = directories[0] ?? []
const difference = first.findIndex((part, index) => directories.some((directory) => directory[index] !== part))
return first.slice(0, difference < 0 ? first.length : difference).join("/") || "."
}

function limit<T extends LearningArea>(areas: T[], maximum: number, overflow: (rest: T[]) => T) {
const ordered = rank(areas)
if (ordered.length <= maximum) return ordered
Expand Down Expand Up @@ -92,6 +99,8 @@ export function analyzeRepositoryGroups(input: readonly string[], referencedFile
if (referenced.has(file)) subgroup.referencedFiles.push(file)
subgroups.set(root, subgroup)
}
// Subgroup IDs combine related source and test areas; they are not filesystem paths.
for (const subgroup of subgroups.values()) subgroup.root = commonDirectory(subgroup.files)
group.subgroups = limit([...subgroups.values()], 20, (rest) => ({
id: `${group.id}/other`,
title: "Other areas",
Expand Down Expand Up @@ -136,7 +145,7 @@ export function applyGroupNames(groups: readonly FunctionalGroup[], response: st
>
return groups.map((group) => {
const title = names[group.id]
return typeof title === "string" ? { ...group, title: title.trim().slice(0, 50) } : group
return typeof title === "string" && title.trim() ? { ...group, title: title.trim().slice(0, 50) } : group
})
} catch {
return [...groups]
Expand Down
28 changes: 28 additions & 0 deletions packages/tui/test/util/repository-functionality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@ import {
} from "../../src/util/repository-functionality"

describe("repository areas", () => {
test("subgroup prompts use a shared real directory instead of the logical group id", () => {
const [group] = analyzeRepositoryGroups([
"packages/api/src/auth/token.ts",
"packages/api/test/auth/token.test.ts",
])
const subgroup = group.subgroups[0]

expect(subgroup.id).toBe("packages/api/auth")
expect(subgroup.root).toBe("packages/api")
const prompt = buildFunctionalGroupPrompt(subgroup, [group])
expect(prompt).toContain("Area root: packages/api\n")
expect(prompt).not.toContain("Area root: packages/api/auth")
expect(prompt).toContain("packages/api/src/auth/token.ts")
expect(prompt).toContain("packages/api/test/auth/token.test.ts")
})

test("subgroup roots preserve nested directories and handle project-root files", () => {
const [nested] = analyzeRepositoryGroups(["packages/api/src/auth/token.ts", "packages/api/src/auth/user.ts"])
expect(nested.subgroups[0].root).toBe("packages/api/src/auth")
const [root] = analyzeRepositoryGroups(["README.md", "LICENSE"])
expect(root.subgroups[0].root).toBe(".")
})

test.each(["", " ", "\t\n"])("retains the structural name for blank model title %j", (title) => {
const groups = analyzeRepositoryGroups(["packages/api/src/main.ts"])
expect(applyGroupNames(groups, JSON.stringify({ "packages/api": title }))).toEqual(groups)
})

test("groups common repository layouts without project-specific names", () => {
const groups = analyzeRepositoryGroups([
"packages/api/src/auth/token.ts",
Expand Down
4 changes: 4 additions & 0 deletions turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
// },
// Only add the package(s) your change actually touches — don't add all
// of them, that defeats the point of keeping CI fast.
"@opencode-ai/tui#test": {
"dependsOn": ["^build"],
"outputs": []
},
"opencode#test": {
"dependsOn": ["^build"],
"outputs": [],
Expand Down
Loading