fix(terminal): stabilize skills table width across Terminal.app and iTerm (#42849)

* Terminal: measure grapheme display width

* Tests: cover grapheme terminal width

* Terminal: wrap table cells by grapheme width

* Tests: cover emoji table alignment

* Terminal: refine table wrapping and width handling

* Terminal: stop shrinking CLI tables by one column

* Skills: use Terminal-safe emoji in list output

* Changelog: note terminal skills table fixes

* Skills: normalize emoji presentation across outputs

* Terminal: consume unsupported escape bytes in tables
This commit is contained in:
Vincent Koc
2026-03-11 09:13:10 -04:00
committed by GitHub
parent 10e6e27451
commit 04e103d10e
32 changed files with 299 additions and 67 deletions

View File

@@ -1,5 +1,5 @@
import type { SkillStatusEntry, SkillStatusReport } from "../agents/skills-status.js";
import { renderTable } from "../terminal/table.js";
import { getTerminalTableWidth, renderTable } from "../terminal/table.js";
import { theme } from "../terminal/theme.js";
import { shortenHomePath } from "../utils.js";
import { formatCliCommand } from "./command-format.js";
@@ -38,8 +38,12 @@ function formatSkillStatus(skill: SkillStatusEntry): string {
return theme.error("✗ missing");
}
function normalizeSkillEmoji(emoji?: string): string {
return (emoji ?? "📦").replaceAll("\uFE0E", "\uFE0F");
}
function formatSkillName(skill: SkillStatusEntry): string {
const emoji = skill.emoji ?? "📦";
const emoji = normalizeSkillEmoji(skill.emoji);
return `${emoji} ${theme.command(skill.name)}`;
}
@@ -95,7 +99,7 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti
}
const eligible = skills.filter((s) => s.eligible);
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const tableWidth = getTerminalTableWidth();
const rows = skills.map((skill) => {
const missing = formatSkillMissingSummary(skill);
return {
@@ -109,7 +113,7 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti
const columns = [
{ key: "Status", header: "Status", minWidth: 10 },
{ key: "Skill", header: "Skill", minWidth: 18, flex: true },
{ key: "Skill", header: "Skill", minWidth: 22 },
{ key: "Description", header: "Description", minWidth: 24, flex: true },
{ key: "Source", header: "Source", minWidth: 10 },
];
@@ -154,7 +158,7 @@ export function formatSkillInfo(
}
const lines: string[] = [];
const emoji = skill.emoji ?? "📦";
const emoji = normalizeSkillEmoji(skill.emoji);
const status = skill.eligible
? theme.success("✓ Ready")
: skill.disabled
@@ -282,7 +286,7 @@ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOp
lines.push("");
lines.push(theme.heading("Ready to use:"));
for (const skill of eligible) {
const emoji = skill.emoji ?? "📦";
const emoji = normalizeSkillEmoji(skill.emoji);
lines.push(` ${emoji} ${skill.name}`);
}
}
@@ -291,7 +295,7 @@ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOp
lines.push("");
lines.push(theme.heading("Missing requirements:"));
for (const skill of missingReqs) {
const emoji = skill.emoji ?? "📦";
const emoji = normalizeSkillEmoji(skill.emoji);
const missing = formatSkillMissingSummary(skill);
lines.push(` ${emoji} ${skill.name} ${theme.muted(`(${missing})`)}`);
}