Skip to content

Commit e431dfb

Browse files
committed
Address PR review comments: rename dispatchMode→repickMode, fix hints text, add tests, update docs
1 parent 237afed commit e431dfb

File tree

5 files changed

+111
-30
lines changed

5 files changed

+111
-30
lines changed

docs/usage/team-grouping.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ github-code-search query "useFeatureFlag" --org fulll \
109109

110110
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.
111111

112+
> **Note:** Per-repo re-picks performed in the TUI (pressing `t` on a `` repo) are **not** encoded in the replay command. They are interactive-only adjustments and must be repeated manually if you re-run the command.
113+
112114
If the combined label is not found (typo, or the section was not formed), a warning is emitted on stderr listing the available combined sections — the run continues without error.
113115

114116
## Re-pick & undo pick
@@ -149,7 +151,7 @@ Pressing `0` or `u` in re-pick mode restores the repo to the combined section it
149151
▶ ◈ fulll/frontend-app
150152
```
151153

152-
In **non-interactive mode**, undoing a pick is implicit: simply omit the `--pick-team` flag for the repo in the replay command.
154+
In **non-interactive mode**, undoing a pick is implicit: simply omit the `--pick-team` flag for that combined section in the replay command.
153155

154156
## Team list cache
155157

src/group.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export function undoPickedRepo(groups: RepoGroup[], repoIndex: number): RepoGrou
204204
i === dstIdx ? { ...s, groups: [...s.groups, restoredRepo] } : s,
205205
);
206206
} else {
207-
// Re-insert at the position of the original source section (best-effort)
207+
// No existing section found — append a new combined section at the end.
208208
sections = [...sections, { label: combinedLabel, groups: [restoredRepo] }];
209209
}
210210

src/render.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1981,3 +1981,82 @@ describe("normalizeScrollOffset", () => {
19811981
expect(normalizeScrollOffset(1, rows, groups, 3)).toBe(1);
19821982
});
19831983
});
1984+
1985+
// ─── renderGroups — re-pick mode hints bar ────────────────────────────────────
1986+
1987+
describe("renderGroups — re-pick mode hints bar", () => {
1988+
it("shows Re-pick: prefix and candidate teams when repickMode is active", () => {
1989+
const groups = [makeGroup("org/repo", ["a.ts"])];
1990+
const rows = buildRows(groups);
1991+
const out = renderGroups(groups, 0, rows, 40, 0, "q", "org", {
1992+
repickMode: {
1993+
active: true,
1994+
repoIndex: 0,
1995+
candidates: ["squad-frontend", "squad-mobile"],
1996+
focusedIndex: 0,
1997+
},
1998+
termWidth: 120,
1999+
});
2000+
const stripped = out.replace(/\x1b\[[0-9;]*m/g, "");
2001+
expect(stripped).toContain("Re-pick:");
2002+
expect(stripped).toContain("squad-frontend");
2003+
expect(stripped).toContain("0/u restore");
2004+
expect(stripped).toContain("Esc/t cancel");
2005+
});
2006+
2007+
it("re-pick hints line visible width never exceeds termWidth", () => {
2008+
// Fix: clip hints to termWidth so the line never wraps — see issue #105.
2009+
const groups = [makeGroup("org/repo", ["a.ts"])];
2010+
const rows = buildRows(groups);
2011+
const termWidth = 60;
2012+
const out = renderGroups(groups, 0, rows, 40, 0, "q", "org", {
2013+
repickMode: {
2014+
active: true,
2015+
repoIndex: 0,
2016+
candidates: ["squad-frontend", "squad-mobile"],
2017+
focusedIndex: 0,
2018+
},
2019+
termWidth,
2020+
});
2021+
const stripped = out.replace(/\x1b\[[0-9;]*m/g, "");
2022+
const repickLine = stripped.split("\n").find((l) => l.includes("Re-pick:"));
2023+
expect(repickLine).toBeDefined();
2024+
expect(repickLine!.length).toBeLessThanOrEqual(termWidth);
2025+
});
2026+
2027+
it("truncates suffix gracefully on very narrow terminal without crashing", () => {
2028+
const groups = [makeGroup("org/repo", ["a.ts"])];
2029+
const rows = buildRows(groups);
2030+
const termWidth = 25; // narrow — full suffix won't fit
2031+
const out = renderGroups(groups, 0, rows, 40, 0, "q", "org", {
2032+
repickMode: {
2033+
active: true,
2034+
repoIndex: 0,
2035+
candidates: ["squad-frontend", "squad-mobile"],
2036+
focusedIndex: 0,
2037+
},
2038+
termWidth,
2039+
});
2040+
const stripped = out.replace(/\x1b\[[0-9;]*m/g, "");
2041+
const repickLine = stripped.split("\n").find((l) => l.includes("Re-pick:"));
2042+
expect(repickLine).toBeDefined();
2043+
expect(repickLine!.length).toBeLessThanOrEqual(termWidth);
2044+
});
2045+
2046+
it("focuses the correct candidate in [ brackets ]", () => {
2047+
const groups = [makeGroup("org/repo", ["a.ts"])];
2048+
const rows = buildRows(groups);
2049+
const out = renderGroups(groups, 0, rows, 40, 0, "q", "org", {
2050+
repickMode: {
2051+
active: true,
2052+
repoIndex: 0,
2053+
candidates: ["squad-alpha", "squad-beta"],
2054+
focusedIndex: 1,
2055+
},
2056+
termWidth: 120,
2057+
});
2058+
const stripped = out.replace(/\x1b\[[0-9;]*m/g, "");
2059+
// focusedIndex=1 → squad-beta is focused → should appear in [ brackets ]
2060+
expect(stripped).toContain("[ squad-beta ]");
2061+
});
2062+
});

src/render.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,8 @@ interface RenderOptions {
320320
candidates: string[];
321321
focusedIndex: number;
322322
};
323-
/** Active team dispatch (re-pick) mode state — when set, shows the re-pick bar in the hints line. */
324-
dispatchMode?: {
323+
/** Active team re-pick mode state — when set, shows the re-pick bar in the hints line. */
324+
repickMode?: {
325325
active: boolean;
326326
repoIndex: number;
327327
candidates: string[];
@@ -465,12 +465,12 @@ export function renderGroups(
465465
}
466466

467467
// Fix: clip hints to termWidth visible chars so the line never wraps — see issue #105.
468-
if (opts.dispatchMode?.active) {
469-
const dm = opts.dispatchMode;
468+
if (opts.repickMode?.active) {
469+
const dm = opts.repickMode;
470470
// Re-pick bar: same layout as pick mode — focused team in [ brackets ], others dimmed.
471471
// Suffix with 0/u undo and Esc/t cancel hints, clipped to one line with horizontal scroll.
472472
const REPICK_PREFIX = "Re-pick: ";
473-
const REPICK_SUFFIX = " 0/u restore ← → move ↵ confirm Esc cancel";
473+
const REPICK_SUFFIX = " 0/u restore ← → move ↵ confirm Esc/t cancel";
474474
const barWidth = Math.max(0, termWidth - REPICK_PREFIX.length - REPICK_SUFFIX.length);
475475
const bar = renderTeamPickHeader(dm.candidates, dm.focusedIndex, barWidth);
476476
const barPlain = stripAnsi(bar);

src/tui.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,9 @@ export async function runInteractive(
200200
* so they are included in the replay command even if no additional interactive picks are made. */
201201
const confirmedPicks: Record<string, string> = { ...initialPickTeams };
202202

203-
// ─── Team dispatch (re-pick) mode state ───────────────────────────────────────
203+
// ─── Team re-pick mode state ──────────────────────────────────────────────────
204204
// Feat: re-pick mode — re-assign a picked (◈) repo to a different team — see issue #87
205-
let dispatchMode: {
205+
let repickMode: {
206206
active: boolean;
207207
repoIndex: number;
208208
candidates: string[];
@@ -250,7 +250,7 @@ export async function runInteractive(
250250
filterTarget,
251251
filterRegex,
252252
teamPickMode: teamPickMode.active ? teamPickMode : undefined,
253-
dispatchMode: dispatchMode.active ? dispatchMode : undefined,
253+
repickMode: repickMode.active ? repickMode : undefined,
254254
});
255255
process.stdout.write(ANSI_CLEAR);
256256
process.stdout.write(rendered);
@@ -310,51 +310,51 @@ export async function runInteractive(
310310

311311
// ── Team re-pick mode key handler ─────────────────────────────────────────
312312
// Feat: re-pick mode — re-assign a picked (◈) repo to a different team — see issue #87
313-
if (dispatchMode.active) {
313+
if (repickMode.active) {
314314
if (key === KEY_CTRL_C) {
315315
process.stdout.write(ANSI_CLEAR);
316316
process.stdin.setRawMode(false);
317317
process.exit(0);
318318
} else if (key === ANSI_ARROW_LEFT) {
319319
// ← — cycle candidate teams backwards
320-
dispatchMode = {
321-
...dispatchMode,
320+
repickMode = {
321+
...repickMode,
322322
focusedIndex:
323-
(dispatchMode.focusedIndex - 1 + dispatchMode.candidates.length) %
324-
dispatchMode.candidates.length,
323+
(repickMode.focusedIndex - 1 + repickMode.candidates.length) %
324+
repickMode.candidates.length,
325325
};
326326
} else if (key === ANSI_ARROW_RIGHT) {
327327
// → — cycle candidate teams forwards
328-
dispatchMode = {
329-
...dispatchMode,
330-
focusedIndex: (dispatchMode.focusedIndex + 1) % dispatchMode.candidates.length,
328+
repickMode = {
329+
...repickMode,
330+
focusedIndex: (repickMode.focusedIndex + 1) % repickMode.candidates.length,
331331
};
332332
} else if (key === KEY_ENTER_CR || key === KEY_ENTER_LF) {
333333
// Enter — confirm re-pick, move repo to the focused candidate team
334-
const targetTeam = dispatchMode.candidates[dispatchMode.focusedIndex];
335-
const g = groups[dispatchMode.repoIndex];
334+
const targetTeam = repickMode.candidates[repickMode.focusedIndex];
335+
const g = groups[repickMode.repoIndex];
336336
groups = moveRepoToSection(groups, g.repoFullName, targetTeam);
337337
const newRows = buildRows(groups, filterPath, filterTarget, filterRegex);
338338
cursor = Math.min(cursor, Math.max(0, newRows.length - 1));
339339
scrollOffset = Math.min(scrollOffset, cursor);
340-
dispatchMode = { active: false, repoIndex: -1, candidates: [], focusedIndex: 0 };
340+
repickMode = { active: false, repoIndex: -1, candidates: [], focusedIndex: 0 };
341341
} else if (key === "0" || key === "u") {
342342
// 0 / u — undo pick, restore repo to its original combined section.
343343
// Remove the confirmedPick entry for the combined label so the replay
344344
// command no longer emits --pick-team for that section.
345-
const combinedLabel = groups[dispatchMode.repoIndex]?.pickedFrom;
345+
const combinedLabel = groups[repickMode.repoIndex]?.pickedFrom;
346346
if (combinedLabel) delete confirmedPicks[combinedLabel];
347-
groups = undoPickedRepo(groups, dispatchMode.repoIndex);
347+
groups = undoPickedRepo(groups, repickMode.repoIndex);
348348
const newRows = buildRows(groups, filterPath, filterTarget, filterRegex);
349349
cursor = Math.min(cursor, Math.max(0, newRows.length - 1));
350350
scrollOffset = Math.min(scrollOffset, cursor);
351-
dispatchMode = { active: false, repoIndex: -1, candidates: [], focusedIndex: 0 };
351+
repickMode = { active: false, repoIndex: -1, candidates: [], focusedIndex: 0 };
352352
} else if (key === "\x1b" && !key.startsWith("\x1b[") && !key.startsWith("\x1b\x1b")) {
353353
// Esc — cancel re-pick mode
354-
dispatchMode = { active: false, repoIndex: -1, candidates: [], focusedIndex: 0 };
354+
repickMode = { active: false, repoIndex: -1, candidates: [], focusedIndex: 0 };
355355
} else if (key === "t") {
356356
// t — toggle re-pick mode off
357-
dispatchMode = { active: false, repoIndex: -1, candidates: [], focusedIndex: 0 };
357+
repickMode = { active: false, repoIndex: -1, candidates: [], focusedIndex: 0 };
358358
}
359359
redraw();
360360
continue;
@@ -535,17 +535,17 @@ export async function runInteractive(
535535
continue;
536536
}
537537

538-
// `t` — on a picked repo (◈, has pickedFrom): enter dispatch mode to re-assign to a
538+
// `t` — on a picked repo (◈, has pickedFrom): enter re-pick mode to re-assign to a
539539
// different team. Otherwise cycle the filter target: path → content → repo → path.
540-
// Feat: team dispatch mode — see issue #86
540+
// Feat: re-pick mode — see issue #87
541541
if (key === "t") {
542542
const isPickedRepo =
543543
groupByTeamPrefix && row?.type === "repo" && !!groups[row.repoIndex]?.pickedFrom;
544544
if (isPickedRepo) {
545-
// Enter dispatch mode — candidates come from the original combined label
545+
// Enter re-pick mode — candidates come from the original combined label
546546
const pickedFrom = groups[row!.repoIndex].pickedFrom!;
547547
const candidates = pickedFrom.split(" + ").map((c) => c.trim());
548-
dispatchMode = { active: true, repoIndex: row!.repoIndex, candidates, focusedIndex: 0 };
548+
repickMode = { active: true, repoIndex: row!.repoIndex, candidates, focusedIndex: 0 };
549549
} else {
550550
// Cycle filter target when not on a picked repo
551551
filterTarget =

0 commit comments

Comments
 (0)