Skip to content

Team pick mode — p on a multi-team section header to assign it to a single team #85

@shouze

Description

@shouze

Problem

When --group-by-team-prefix groups repos that belong to several teams simultaneously, a combined section like squad-frontend + squad-mobile appears. For downstream tooling such as github-issue-ops, each section must have a single unambiguous owner. There is currently no way to resolve this ambiguity interactively.

Proposed UX

Pressing p on any section header whose label contains + (i.e. a multi-team section) enters team pick mode — analogous to filter mode (f):

  1. The section header re-renders the team names as a horizontal list. The currently focused team is highlighted (bold + full colour, wrapped in [ ]); the others are dimmed.
  2. / cycle through the candidate teams.
  3. Enter confirms the pick: the section label collapses to the single chosen team, and all repos in that section are reassigned to it. Moved repos are annotated with a badge in the TUI.
  4. Esc cancels — no change.

Sections that already have a single team label are not eligible for pick mode (pressing p on them does nothing).

Navigation note: Section header rows are navigable (↑/↓ can land on them — do not skip them). The cursor highlights the section header with bgMagenta + ── prefix. Pressing p while the cursor is on a multi-team section header enters pick mode.

CLI — replay

A new repeatable option --pick-team <combined>=<chosen> encodes the choice for non-interactive mode. Example:

github-code-search query "..." --org myorg \
  --group-by-team-prefix squad- \
  --pick-team "squad-frontend + squad-mobile"=squad-frontend

The = separator is safe because it never appears in team slugs or section labels. The combined label must be quoted in the shell when it contains spaces.

When --pick-team is given for a label that does not exist (typo, or the combined section was not formed), a warning is emitted on stderr listing the available combined sections — the run continues without error.

CLI picks are propagated to the interactive TUI via an initialPickTeams parameter so they appear in the replay command when the user switches to interactive mode.

Acceptance criteria

  • p on a single-team section header does nothing
  • p on a multi-team section header enters pick mode; / navigate with [ focused ] / dimmed rendering
  • Enter confirms: section label updates, repos are reassigned, moved repos show a badge in the TUI
  • Esc cancels with no side effect
  • --pick-team "A + B"=A in non-interactive mode produces identical output to the TUI confirmation
  • The replay command emits --pick-team when a pick was confirmed in the TUI or via CLI
  • applyTeamPick returns the same object reference when the combined label is not found (identity check — no silent mutation)
  • --pick-team with an unmatched label emits a stderr warning listing available combined labels; the run continues
  • CLI picks are passed to runInteractive via initialPickTeams; confirmedPicks is pre-seeded from it so picks appear in the replay command
  • Repos moved by applyTeamPick have pickedFrom set to the original combined label; a badge is shown next to the repo name in the TUI
  • Section header rows are navigable (↑/↓ lands on them); p only activates pick mode when sectionLabel.includes(" + ") is true
  • Multiple --pick-team flags targeting the same destination team aggregate all repos correctly
  • Combined label order must be alphabetical ("squad-a + squad-b" — same order produced by groupByTeamPrefix); mismatched order triggers a stderr warning
  • bun test, bun run lint, bun run knip all pass
  • bun run docs:build completes without errors

Edge cases

Scenario Expected behaviour
p on a single-team section No-op
--pick-team label not found Stderr warning with list of available combined labels; run continues
Multiple --pick-team targeting the same destination team All repos merged into that team's section
3-team combined section (e.g. a + b + c) applyTeamPick handles correctly; all 3 candidates shown in pick bar
Combined label order mismatch ("b + a" instead of "a + b") Warning on stderr — label must match alphabetical order from groupByTeamPrefix
--pick-team without --group-by-team-prefix Section list is empty; stderr warning for each unmatched pick; output unchanged
CLI picks passed to TUI confirmedPicks pre-seeded; picks appear in replay command
Repo moved via pick in TUI pickedFrom set to original combined label; badge shown next to repo name

Implementation guide

Recommended order:

  1. src/types.ts — add pickedFrom?: string to RepoGroup:

    /** Set by applyTeamPick on repos moved from a combined section.
     * Stores the original combined label for future split-mode use. */
    pickedFrom?: string;
  2. src/group.ts — add two pure helpers:

    applyTeamPick(sections: TeamSection[], combinedLabel: string, chosenTeam: string): TeamSection[]

    Filters out the matching combined section and re-inserts its repos (with pickedFrom set to the original combined label) into the chosen team's existing section (or creates a new single-team section). Returns the same reference when the combined label is not found — callers must identity-check the return value.

    rebuildTeamSections(groups: RepoGroup[]): TeamSection[]

    Reconstructs TeamSection[] from a flat RepoGroup[] using sectionLabel markers (used internally by applyTeamPick).

  3. src/render/team-pick.ts (new file) — pure function:

    renderTeamPickHeader(candidateTeams: string[], focusedIndex: number): string

    Returns a string where the focused team is wrapped in [ ] + bold + full colour; the others are dimmed (picocolors only).

    ⚠️ Do NOT re-export this symbol from src/render.ts — it is consumed only internally inside render.ts. Re-exporting it would cause knip to flag it as an unused export.

  4. src/render.ts — import renderTeamPickHeader directly from render/team-pick.ts (internal use). Add a teamPickMode branch in section-row rendering:

    • Pick mode active on this section → render renderTeamPickHeader bar
    • Cursor on this section (no pick mode) → bgMagenta(bold("── <label> ")) + [p: pick team] hint when multi-team
    • Default → pc.magenta(pc.bold("\n── <label> "))

    Add a badge (dimmed) after the repo name when group.pickedFrom is set.

  5. src/tui.ts — add teamPickMode state:

    { active: boolean; sectionLabel: string; candidates: string[]; focusedIndex: number }

    Key bindings (only when teamPickMode.active === true):

    • / — move focusedIndex
    • Enter — call applyTeamPick, exit mode, rebuild rows
    • Esc — exit mode without change

    p on a section row whose label contains " + " → split by " + " to get candidates, enter pick mode with focusedIndex: 0.

    Add initialPickTeams: Record<string, string> = {} parameter to runInteractive; pre-seed confirmedPicks with { ...initialPickTeams }.

    ⚠️ Do NOT skip section rows in ↑/↓ navigation. Any while (row.type === "section") skip loop must be removed — section headers must be reachable by the cursor, otherwise p would be inaccessible.

  6. src/output.ts — add pickTeams?: Record<string, string> to ReplayOptions; in buildReplayCommand, emit --pick-team "<combined>"=<chosen> per entry.

  7. github-code-search.ts — add --pick-team <combined>=<chosen> (repeatable via .option + collect); parse each = pair; after groupByTeamPrefix and before flattenTeamSections, call applyTeamPick for each pair. Identity-check the return: if unchanged, emit a stderr warning listing available combined labels. Pass resolved picks to runInteractive via initialPickTeams.

  8. Tests

    • src/group.test.tsapplyTeamPick (two-team, three-team, identity check when label not found, pickedFrom field set on moved repos), rebuildTeamSections
    • src/render/team-pick.test.tsrenderTeamPickHeader (focused bracket style, unfocused dim, 3-team, ANSI codes)
    • src/output.test.ts — verify --pick-team is emitted in the replay when pickTeams is set

Documentation

Three files must be updated:


1. docs/usage/team-grouping.md

Correct the existing note in ## Interactive mode with sections — section headers are navigable (remove any statement that ↑/↓ skips them).

Add a ## Team pick mode section after ## Interactive mode with sections:

## Team pick mode

When a section header shows multiple teams (e.g. `squad-frontend + squad-mobile`), pressing `p` on it enters **team pick mode**. Use this to assign the entire section to a single owner before exporting results to downstream tooling.

### In the TUI

The section header switches to a horizontal pick bar:

── [ squad-frontend ] squad-mobile


The highlighted team (bold, full colour, wrapped in `[ ]`) is the current selection. The others are dimmed.

| Key         | Action                                    |
| ----------- | ----------------------------------------- |
| `←` / `→`   | Move focus between candidate teams        |
| `Enter`     | Confirm — section label updates in place  |
| `Esc`       | Cancel — no change                        |

`p` on a section that already has a single team label does nothing.

Repos moved into a team by pick mode are annotated with a `◈` badge next to their name.

### Non-interactive — `--pick-team`

github-code-search query "useFeatureFlag" --org fulll
--group-by-team-prefix squad-
--pick-team "squad-frontend + squad-mobile"=squad-frontend


The flag is repeatable — add one `--pick-team` per combined section to resolve. The replay command emits `--pick-team` automatically when a pick was confirmed in the TUI.

::: tip Combined with --dispatch
`--pick-team` resolves ownership at the **section level** (all repos in the section move to one team). For finer-grained control — assigning individual repos or extracts to different teams — see `--dispatch` (#86).
:::

2. docs/reference/keyboard-shortcuts.md

Add a ## Team ownership section after the ## Filtering section:

## Team ownership

Available only when `--group-by-team-prefix` is active.

| Key | Action |
| --- | --- |
| `p` | On a **multi-team** section header: enter team pick mode to assign the section to a single owner. Does nothing on single-team section headers. |

### Pick mode bindings

When pick mode is active (after pressing `p` on a multi-team section header):

| Key       | Action                                                    |
| --------- | --------------------------------------------------------- |
| `` / `` | Move focus between candidate teams (highlighted / dimmed) |
| `Enter`   | Confirm the pick and exit pick mode                       |
| `Esc`     | Cancel and exit pick mode without changes                 |

3. docs/reference/cli-options.md

In the ## Search options table, add a row for --pick-team after the --group-by-team-prefix row:

| `--pick-team <combined>=<chosen>` | string (repeatable) ||| Assign a combined team section to a single owner. Format: `"<combined label>"=<chosen team>`. Repeatable — one flag per combined section. Only applies with `--group-by-team-prefix`. See [Team pick mode](/usage/team-grouping#team-pick-mode). |

Notes

  • Parse candidate teams from the section label by splitting on " + " (space-surrounded plus sign).
  • picocolors is the only styling dependency — do not add chalk or similar.
  • Keep knip clean: every exported symbol must be used. renderTeamPickHeader must not be re-exported from render.ts.
  • Add a comment above the new tui.ts pick-mode block: // Feat: team pick mode — resolve multi-team section ownership — see issue #85
  • RepoGroup.pickedFrom is the foundation for a future split mode feature (issue Team dispatch — split a multi-team section by assigning each repo/extract to exactly one team #86) that will let individual repos be re-assigned across teams independently.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions