From 89457ad3d4845488e42fda7a9e4a0c3810bfcf6a Mon Sep 17 00:00:00 2001 From: yswcyswc Date: Fri, 9 Oct 2026 14:08:28 -0400 Subject: [PATCH 1/3] chore(tui): include terminal tests in CI --- turbo.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/turbo.json b/turbo.json index f59711fa..7544901b 100644 --- a/turbo.json +++ b/turbo.json @@ -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": [], From 972aae90f2e8c669f45f55457219435ed9af9ef0 Mon Sep 17 00:00:00 2001 From: yswcyswc Date: Fri, 9 Oct 2026 14:08:28 -0400 Subject: [PATCH 2/3] docs(tui): organize group map usage and testing guide --- UserGuide.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/UserGuide.md b/UserGuide.md index cd2aea47..20cc7cff 100644 --- a/UserGuide.md +++ b/UserGuide.md @@ -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 From 2df8cfd5f30a6179053cff350476f75b60144265 Mon Sep 17 00:00:00 2001 From: yswcyswc Date: Fri, 9 Oct 2026 15:11:45 -0400 Subject: [PATCH 3/3] fix(tui): preserve valid group roots and fallback names --- .../tui/src/util/repository-functionality.ts | 11 +++++++- .../util/repository-functionality.test.ts | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/tui/src/util/repository-functionality.ts b/packages/tui/src/util/repository-functionality.ts index bbb13ef5..ef372798 100644 --- a/packages/tui/src/util/repository-functionality.ts +++ b/packages/tui/src/util/repository-functionality.ts @@ -49,6 +49,13 @@ function rank(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(areas: T[], maximum: number, overflow: (rest: T[]) => T) { const ordered = rank(areas) if (ordered.length <= maximum) return ordered @@ -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", @@ -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] diff --git a/packages/tui/test/util/repository-functionality.test.ts b/packages/tui/test/util/repository-functionality.test.ts index 7e8d8740..3b558ebf 100644 --- a/packages/tui/test/util/repository-functionality.test.ts +++ b/packages/tui/test/util/repository-functionality.test.ts @@ -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",