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
42 changes: 17 additions & 25 deletions src/commands/guided.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,8 @@ export function buildTravelGroups(report: ScanReport, others: ScanReport[] = [])
}),
});
}
if (report.excluded.length > 0) {
groups.push({
title: "Never leaves this machine",
locked: true,
items: report.excluded.map((entry) => ({
value: entry.path,
label: entry.path.split("/").pop() ?? entry.path,
hint: entry.reason,
})),
});
}
// Exclusions no longer occupy the picker (the owner's call): the review's
// closing line accounts for them where the leaves-the-machine story lives.
return groups;
}

Expand Down Expand Up @@ -170,19 +161,20 @@ function factsLines(reports: ScanReport[]): string[] {
const title = titles[report.agent].padEnd(13);
const userItems = report.items.filter((item) => item.scope === "user");
const counts: string[] = [];
for (const [kind, label] of [
["skill", "skills"],
["subagent", "subagents"],
["command", "commands"],
["memory", "memory"],
["settings", "settings"],
["hook", "hooks"],
["plugin", "plugins"],
["mcp_server", "MCP servers"],
for (const [kind, singular, plural] of [
["skill", "skill", "skills"],
["subagent", "subagent", "subagents"],
["command", "command", "commands"],
["memory", "memory", "memory"],
["settings", "settings", "settings"],
["hook", "hook", "hooks"],
["plugin", "plugin", "plugins"],
["mcp_server", "MCP server", "MCP servers"],
] as const) {
const total = userItems.filter((item) => item.kind === kind).length;
if (total === 0) continue;
counts.push(kind === "settings" || kind === "memory" ? label : `${total} ${label}`);
if (kind === "settings" || kind === "memory") counts.push(singular);
else counts.push(`${total} ${total === 1 ? singular : plural}`);
}
return [`${title} ${counts.length > 0 ? counts.join(" · ") : "nothing to sync"}`];
});
Expand Down Expand Up @@ -220,7 +212,7 @@ export async function runGuided(io: CommandIo, mode: GuidedMode, overrides: Guid

const groups = buildTravelGroups(report, reports.slice(1));
const hookGroup = buildHookGroup(report);
if (groups.every((group) => group.locked === true) && hookGroup === null) {
if (groups.length === 0 && hookGroup === null) {
io.err("Nothing to sync yet. Run `agent-sync scan` to see what agent-sync looks for.");
return 0;
}
Expand All @@ -229,7 +221,7 @@ export async function runGuided(io: CommandIo, mode: GuidedMode, overrides: Guid
try {
ui.intro("found on this machine", `carry your agent setup anywhere ${"·"} v${__PKG_VERSION__}`, factsLines(reports));

const travel = await ui.groupMultiselect("What should travel?", groups);
const travel = await ui.groupMultiselect("What goes in the bundle?", groups);
if (travel === null) return 2;
const chosen = new Set(travel);
// Plugins are opt-in (--plugin), everything else is opt-out (--skip), so an
Expand All @@ -243,9 +235,9 @@ export async function runGuided(io: CommandIo, mode: GuidedMode, overrides: Guid
let hooks: string[] = [];
if (hookGroup !== null) {
const picked = await ui.groupMultiselect(
"Which hooks may travel?",
"Which hooks go in the bundle?",
[hookGroup],
"hooks run shell commands on the target \u00b7 none travel unless you pick them",
"hooks run shell commands on the target \u00b7 none go unless you pick them",
);
if (picked === null) return 2;
hooks = picked;
Expand Down
4 changes: 2 additions & 2 deletions src/tui/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export function renderMulti<T>(state: MultiState<T>, theme: Theme, maxRows: numb
const visible = visibleItems(state);
const lines: string[] = [];

const chip = theme.paint("inverse", ` ${state.selected.size} of ${state.items.length} picked `);
const chip = theme.paint("highlight", ` ${state.selected.size} of ${state.items.length} picked `);
lines.push(`${theme.paint("accent", g.stepActive)} ${theme.paint("bright", state.message)} ${chip}`);
if (state.filtering || state.query.length > 0) {
lines.push(
Expand Down Expand Up @@ -247,7 +247,7 @@ export function renderMulti<T>(state: MultiState<T>, theme: Theme, maxRows: numb
const mark = state.selected.has(item.id) ? g.pickOn : g.pickOff;
if (active) {
const content = ` ${mark} ${item.label}`.padEnd(labelWidth).slice(0, labelWidth);
rows.push(`${bar} ${theme.paint("inverse", content)}`);
rows.push(`${bar} ${theme.paint("highlight", content)}`);
} else {
const painted = state.selected.has(item.id) ? theme.paint("ok", mark) : theme.paint("dim", mark);
rows.push(`${bar} ${painted} ${item.label}`);
Expand Down
15 changes: 13 additions & 2 deletions src/tui/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function colorEnabled(options: ThemeEnvironment = {}): boolean {
return options.isTTY ?? process.stderr.isTTY === true;
}

export type Style = "accent" | "dim" | "bright" | "warn" | "bad" | "ok" | "strike" | "inverse";
export type Style = "accent" | "dim" | "bright" | "warn" | "bad" | "ok" | "strike" | "inverse" | "highlight";

const CODES: Record<Style, [string, string]> = {
accent: ["\x1b[36m", "\x1b[39m"],
Expand All @@ -115,8 +115,18 @@ const CODES: Record<Style, [string, string]> = {
ok: ["\x1b[32m", "\x1b[39m"],
strike: ["\x1b[9m", "\x1b[29m"],
inverse: ["\x1b[7m", "\x1b[27m"],
highlight: ["\x1b[48;5;236m", "\x1b[49m"],
};

// A washed-out inverse block reads as glare on dark terminals; where the
// terminal advertises 256 colors the highlight is a quiet dark-gray band,
// and elsewhere it falls back to bold rather than inverse.
export function supports256Colors(options: ThemeEnvironment = {}): boolean {
const env = options.env ?? process.env;
if (env.COLORTERM !== undefined && env.COLORTERM !== "") return true;
return (env.TERM ?? "").includes("256color");
}

export interface Theme {
glyphs: Glyphs;
unicode: boolean;
Expand All @@ -127,13 +137,14 @@ export interface Theme {
export function createTheme(options: ThemeEnvironment = {}): Theme {
const unicode = unicodeSupported(options);
const color = colorEnabled(options);
const deep = supports256Colors(options);
return {
glyphs: unicode ? UNICODE : ASCII,
unicode,
color,
paint(style, text) {
if (!color || text.length === 0) return text;
const [open, close] = CODES[style];
const [open, close] = style === "highlight" && !deep ? CODES.bright : CODES[style];
return `${open}${text}${close}`;
},
};
Expand Down
6 changes: 4 additions & 2 deletions test/guided.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,10 @@ test("guided picker flow: accepting every default writes setup.tgz and echoes th
assert.ok(!("hooks" in settings), "hooks are opt-in and none were selected");

const rendered = fake.chunks.join("");
assert.match(rendered, /never leaves? this machine/);
assert.ok(!/never leaves? this machine/.test(rendered), "exclusions left the picker by design");
assert.match(rendered, /What goes in the bundle\?/);
assert.match(rendered, /This is what leaves the machine/);
assert.match(rendered, /excluded item(s)? stayed behind/);
assert.match(rendered, /agent-sync export setup\.tgz/);
});

Expand All @@ -376,7 +378,7 @@ test("guided plain flow: scripted answers write the bundle and close stdin", asy
assert.ok(existsSync(join(destDir, "setup.tgz")));
const transcript = said.join("");
assert.match(transcript, /plain mode/);
assert.match(transcript, /never leave|never included/i);
assert.match(transcript, /excluded item(s)? stayed behind/);
assert.match(transcript, /agent-sync export setup\.tgz/);
});

Expand Down
3 changes: 3 additions & 0 deletions test/tui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ test("theme: unicode detection and color ladder", () => {
assert.equal(ascii.paint("accent", "hi"), "hi");
const colored = createTheme({ env: { FORCE_COLOR: "1" }, platform: "darwin", isTTY: true });
assert.match(colored.paint("accent", "hi"), /\x1b\[36mhi\x1b\[39m/);
const deep = createTheme({ env: { FORCE_COLOR: "1", TERM: "xterm-256color" }, platform: "darwin", isTTY: true });
assert.match(deep.paint("highlight", "row"), /\x1b\[48;5;236mrow\x1b\[49m/, "256-color terminals get the quiet band");
assert.match(colored.paint("highlight", "row"), /\x1b\[1mrow\x1b\[22m/, "basic color falls back to bold, never inverse");
});

test("visual width and truncation never let a line exceed budget", () => {
Expand Down