From cf95b05da3c9a013da8927f9d47ff51566797030 Mon Sep 17 00:00:00 2001
From: AgileInnov8tor <127654909+AgileInnov8tor@users.noreply.github.com>
Date: Sat, 22 Aug 2026 17:12:05 +0700
Subject: [PATCH 1/2] a11y: colorblind-safe default palette with dual-encoded
status
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replace Tokyo Night / 16-color traffic lights with a shared Okabe–Ito
semantic theme (truecolor plus 16-color fallback). Status uses words or
ASCII glyphs as well as hue; provider labels stay uncolored. --no-color
and NO_COLOR still strip ANSI.
Preview: docs/cvd-palette-preview.html
---
.gitignore | 1 +
docs/cvd-palette-preview.html | 187 ++++++++++++++++++++++++++++++++++
src/formatter-core.ts | 69 ++-----------
src/formatter.test.ts | 32 +++++-
src/security-auditor.test.ts | 3 +-
src/security-auditor.ts | 8 +-
src/utils/colors.test.ts | 96 +++++++++++++++++
src/utils/colors.ts | 123 ++++++++++++++++++++--
src/views/config.test.tsx | 4 +-
src/views/config.tsx | 2 +-
src/views/dashboard.test.tsx | 6 +-
src/views/dashboard.tsx | 12 ++-
12 files changed, 450 insertions(+), 93 deletions(-)
create mode 100644 docs/cvd-palette-preview.html
create mode 100644 src/utils/colors.test.ts
diff --git a/.gitignore b/.gitignore
index 10183457..c6faf082 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
node_modules/
dist/
+.worktrees/
# Temp files from runInlineTs (tests/e2e). Cleaned up by a finally block
# on success; listing them here keeps a SIGKILL'd test runner from leaving
diff --git a/docs/cvd-palette-preview.html b/docs/cvd-palette-preview.html
new file mode 100644
index 00000000..5efdcece
--- /dev/null
+++ b/docs/cvd-palette-preview.html
@@ -0,0 +1,187 @@
+
+
+
+
+ asm CVD palette preview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TUI dashboard (rainbow stripped; dupes dual-encoded)
+
+
agent-skill-manager
+
Total: 42 (38 unique) Global: 30 Project: 12 Symlinks: 4 Tools: 6 Dupes: ! 3
+
Both Global Project — Claude, Codex +4
+
press / to search...
+
❯ skill-auto-improver 0.3.1 medium Claude Code
+ skill-creator 1.2.0
low Codex
+ beads 1.4.0
high Grok CLI
+
+
+
+
+ TUI config (ASCII on/off)
+
+
Configuration
+
Config: ~/.config/agent-skill-manager/config.json
+
Tools (Enter to toggle, e to edit config file):
+
❯ on Claude Code ~/.claude/skills
+
on Codex ~/.codex/skills
+
off Grok CLI ~/.grok/skills
+
+
+
+
+ TUI uninstall confirm
+
+
Uninstall: old-skill
+
The following will be removed:
+
~/.claude/skills/old-skill
+
Yes, uninstall Cancel
+
+
+
+
+ CLI asm list (effort words + hue; no provider hues)
+
+Name Version Effort Tool
+skill-auto-improver 0.3.1 medium [Claude Code]
+skill-creator 1.2.0 low [Codex]
+beads 1.4.0 high [Grok CLI]
+eval-max 0.1.0 max [Oh My Pi]
+
+
+
+
+ CLI tools + security badges (no bg fills)
+
+Allowed tools: Bash ! Read WebFetch
+
+ +-- Security Audit --------------------------+
+ | clean-skill [SAFE] |
+ | risky-skill [!! DANGEROUS] |
+ | noisy-skill [WARNING] |
+ | odd-skill [CAUTION] |
+
+
+
+
+
+
diff --git a/src/formatter-core.ts b/src/formatter-core.ts
index 8cd15289..09f13024 100644
--- a/src/formatter-core.ts
+++ b/src/formatter-core.ts
@@ -7,38 +7,9 @@ import type { SkillInfo } from "./utils/types";
import { formatTokenCount } from "./utils/token-count";
import { formatInvocability } from "./utils/frontmatter";
+import { ansi, useColor } from "./utils/colors";
-// ─── Color helpers ──────────────────────────────────────────────────────────
-
-export const useColor = (): boolean => {
- if (process.env.NO_COLOR !== undefined) return false;
- if (globalThis.__CLI_NO_COLOR) return false;
- if (!process.stdout.isTTY) return false;
- return true;
-};
-
-const ansi = {
- bold: (s: string) => (useColor() ? `\x1b[1m${s}\x1b[0m` : s),
- cyan: (s: string) => (useColor() ? `\x1b[36m${s}\x1b[0m` : s),
- green: (s: string) => (useColor() ? `\x1b[32m${s}\x1b[0m` : s),
- yellow: (s: string) => (useColor() ? `\x1b[33m${s}\x1b[0m` : s),
- dim: (s: string) => (useColor() ? `\x1b[2m${s}\x1b[0m` : s),
- white: (s: string) => (useColor() ? `\x1b[37m${s}\x1b[0m` : s),
- red: (s: string) => (useColor() ? `\x1b[31m${s}\x1b[0m` : s),
- blue: (s: string) => (useColor() ? `\x1b[34m${s}\x1b[0m` : s),
- blueBold: (s: string) => (useColor() ? `\x1b[34;1m${s}\x1b[0m` : s),
- magenta: (s: string) => (useColor() ? `\x1b[35m${s}\x1b[0m` : s),
- bgDim: (s: string) => (useColor() ? `\x1b[48;5;236m${s}\x1b[0m` : s),
- bgRed: (s: string) => (useColor() ? `\x1b[41m\x1b[37m\x1b[1m${s}\x1b[0m` : s),
- bgYellow: (s: string) =>
- useColor() ? `\x1b[43m\x1b[30m\x1b[1m${s}\x1b[0m` : s,
- bgGreen: (s: string) =>
- useColor() ? `\x1b[42m\x1b[30m\x1b[1m${s}\x1b[0m` : s,
- bgCyan: (s: string) =>
- useColor() ? `\x1b[46m\x1b[30m\x1b[1m${s}\x1b[0m` : s,
-};
-
-export { ansi };
+export { ansi, useColor };
// ─── Effort colors ─────────────────────────────────────────────────────────
@@ -58,37 +29,13 @@ export function colorEffort(effort: string | undefined): string {
}
}
-// ─── Provider colors ───────────────────────────────────────────────────────
-
-const PROVIDER_COLORS: Record string> = {
- claude: ansi.blueBold,
- codex: ansi.cyan,
- "codex-plugin": ansi.cyan,
- openclaw: ansi.yellow,
- agents: ansi.green,
- custom: ansi.magenta,
- cursor: ansi.blue,
- windsurf: ansi.cyan,
- cline: ansi.green,
- roocode: ansi.magenta,
- continue: ansi.yellow,
- copilot: ansi.white,
- aider: ansi.red,
- opencode: ansi.cyan,
- zed: ansi.blue,
- augment: ansi.green,
- amp: ansi.yellow,
-};
-
-export function colorProvider(provider: string, label: string): string {
- const colorFn = PROVIDER_COLORS[provider] || ansi.dim;
- return colorFn(label);
+// Provider names are the signal; hue is not used.
+export function colorProvider(_provider: string, label: string): string {
+ return label;
}
-export function providerBadge(provider: string, label: string): string {
- if (!useColor()) return `[${label}]`;
- const colorFn = PROVIDER_COLORS[provider] || ansi.dim;
- return colorFn(`[${label}]`);
+export function providerBadge(_provider: string, label: string): string {
+ return `[${label}]`;
}
// ─── Path shortening ───────────────────────────────────────────────────────
@@ -728,7 +675,7 @@ export const HIGH_RISK_TOOLS = new Set([
export const MEDIUM_RISK_TOOLS = new Set(["WebFetch", "WebSearch"]);
export function colorTool(tool: string): string {
- if (HIGH_RISK_TOOLS.has(tool)) return ansi.red(tool);
+ if (HIGH_RISK_TOOLS.has(tool)) return ansi.red(`${tool} !`);
if (MEDIUM_RISK_TOOLS.has(tool)) return ansi.yellow(tool);
return ansi.green(tool);
}
diff --git a/src/formatter.test.ts b/src/formatter.test.ts
index 2c8def00..7609565c 100644
--- a/src/formatter.test.ts
+++ b/src/formatter.test.ts
@@ -527,6 +527,28 @@ describe("colorProvider", () => {
});
});
+describe("colorProvider drops per-provider hues", () => {
+ test("does not wrap labels in ANSI when color is on", () => {
+ delete (globalThis as { __CLI_NO_COLOR?: boolean }).__CLI_NO_COLOR;
+ const originalIsTTY = process.stdout.isTTY;
+ Object.defineProperty(process.stdout, "isTTY", {
+ value: true,
+ configurable: true,
+ });
+ process.env.COLORTERM = "truecolor";
+ try {
+ expect(colorProvider("claude", "Claude Code")).toBe("Claude Code");
+ expect(colorProvider("codex", "Codex")).toBe("Codex");
+ } finally {
+ Object.defineProperty(process.stdout, "isTTY", {
+ value: originalIsTTY,
+ configurable: true,
+ });
+ delete process.env.COLORTERM;
+ }
+ });
+});
+
// ─── formatGroupedTable ───────────────────────────────────────────────────
describe("formatGroupedTable", () => {
@@ -851,10 +873,10 @@ describe("colorTool", () => {
});
test("returns tool name for high-risk tools (no ANSI in no-color mode)", () => {
- expect(colorTool("Bash")).toBe("Bash");
- expect(colorTool("Write")).toBe("Write");
- expect(colorTool("Edit")).toBe("Edit");
- expect(colorTool("NotebookEdit")).toBe("NotebookEdit");
+ expect(colorTool("Bash")).toBe("Bash !");
+ expect(colorTool("Write")).toBe("Write !");
+ expect(colorTool("Edit")).toBe("Edit !");
+ expect(colorTool("NotebookEdit")).toBe("NotebookEdit !");
});
test("returns tool name for medium-risk tools", () => {
@@ -897,7 +919,7 @@ describe("formatAllowedTools", () => {
test("joins tools with double-space separator", () => {
const result = formatAllowedTools(["Bash", "Read", "Grep"]);
- expect(result).toBe("Bash Read Grep");
+ expect(result).toBe("Bash ! Read Grep");
});
test("handles single tool", () => {
diff --git a/src/security-auditor.test.ts b/src/security-auditor.test.ts
index 22fa11a6..f51b05ac 100644
--- a/src/security-auditor.test.ts
+++ b/src/security-auditor.test.ts
@@ -792,7 +792,8 @@ describe("formatSecurityReport", () => {
expect(output).toContain("Security Audit");
expect(output).toContain("clean");
- expect(output).toContain("SAFE");
+ expect(output).toContain("[SAFE]");
+ expect(output).not.toContain(" SAFE ");
expect(output).toContain("No suspicious patterns");
});
diff --git a/src/security-auditor.ts b/src/security-auditor.ts
index bbd398c1..17529c2c 100644
--- a/src/security-auditor.ts
+++ b/src/security-auditor.ts
@@ -651,13 +651,13 @@ const SEVERITY_ORDER: Record = {
function verdictBadge(verdict: SecurityVerdict): string {
switch (verdict) {
case "safe":
- return color.bgGreen(" SAFE ");
+ return color.green("[SAFE]");
case "caution":
- return color.bgCyan(" CAUTION ");
+ return color.cyan("[CAUTION]");
case "warning":
- return color.bgYellow(" WARNING ");
+ return color.yellow("[WARNING]");
case "dangerous":
- return color.bgRed(" DANGEROUS ");
+ return color.red("[!! DANGEROUS]");
}
}
diff --git a/src/utils/colors.test.ts b/src/utils/colors.test.ts
new file mode 100644
index 00000000..7ad1bae8
--- /dev/null
+++ b/src/utils/colors.test.ts
@@ -0,0 +1,96 @@
+import { afterEach, describe, expect, test } from "vitest";
+import { ansi, paint, roles, useColor, useTrueColor } from "./colors";
+
+const originalIsTTY = process.stdout.isTTY;
+const originalColorTerm = process.env.COLORTERM;
+const originalNoColor = process.env.NO_COLOR;
+
+function enableColor(truecolor: boolean): void {
+ delete process.env.NO_COLOR;
+ delete (globalThis as { __CLI_NO_COLOR?: boolean }).__CLI_NO_COLOR;
+ Object.defineProperty(process.stdout, "isTTY", {
+ value: true,
+ configurable: true,
+ });
+ if (truecolor) process.env.COLORTERM = "truecolor";
+ else delete process.env.COLORTERM;
+}
+
+afterEach(() => {
+ Object.defineProperty(process.stdout, "isTTY", {
+ value: originalIsTTY,
+ configurable: true,
+ });
+ if (originalColorTerm === undefined) delete process.env.COLORTERM;
+ else process.env.COLORTERM = originalColorTerm;
+ if (originalNoColor === undefined) delete process.env.NO_COLOR;
+ else process.env.NO_COLOR = originalNoColor;
+ delete (globalThis as { __CLI_NO_COLOR?: boolean }).__CLI_NO_COLOR;
+});
+
+describe("useColor / useTrueColor", () => {
+ test("NO_COLOR disables color even on a TTY", () => {
+ enableColor(true);
+ process.env.NO_COLOR = "";
+ expect(useColor()).toBe(false);
+ });
+
+ test("__CLI_NO_COLOR disables color", () => {
+ enableColor(true);
+ globalThis.__CLI_NO_COLOR = true;
+ expect(useColor()).toBe(false);
+ });
+
+ test("COLORTERM=truecolor enables 24-bit", () => {
+ enableColor(true);
+ expect(useTrueColor()).toBe(true);
+ });
+
+ test("COLORTERM=24bit enables 24-bit", () => {
+ enableColor(false);
+ process.env.COLORTERM = "24bit";
+ expect(useTrueColor()).toBe(true);
+ });
+
+ test("missing COLORTERM is 16-color fallback", () => {
+ enableColor(false);
+ expect(useTrueColor()).toBe(false);
+ });
+});
+
+describe("paint", () => {
+ test("returns the string unchanged when color is off", () => {
+ globalThis.__CLI_NO_COLOR = true;
+ expect(paint("danger", "x")).toBe("x");
+ expect(ansi.red("x")).toBe("x");
+ });
+
+ test("truecolor uses 24-bit SGR from the role hex", () => {
+ enableColor(true);
+ expect(paint("accent", "hi")).toBe("\x1b[38;2;86;180;233mhi\x1b[0m");
+ expect(paint("success", "hi")).toBe("\x1b[38;2;43;196;138mhi\x1b[0m");
+ expect(paint("warning", "hi")).toBe("\x1b[38;2;230;159;0mhi\x1b[0m");
+ expect(paint("danger", "hi")).toBe("\x1b[38;2;213;94;0mhi\x1b[0m");
+ expect(paint("special", "hi")).toBe("\x1b[38;2;204;121;167mhi\x1b[0m");
+ });
+
+ test("16-color fallback uses the locked SGR map", () => {
+ enableColor(false);
+ expect(paint("accent", "hi")).toBe("\x1b[36mhi\x1b[0m");
+ expect(paint("success", "hi")).toBe("\x1b[32mhi\x1b[0m");
+ expect(paint("warning", "hi")).toBe("\x1b[33mhi\x1b[0m");
+ expect(paint("danger", "hi")).toBe("\x1b[31mhi\x1b[0m");
+ expect(paint("special", "hi")).toBe("\x1b[35mhi\x1b[0m");
+ });
+
+ test("role hex values match the locked Q10 map", () => {
+ expect(roles.accent).toBe("#56B4E9");
+ expect(roles.success).toBe("#2BC48A");
+ expect(roles.warning).toBe("#E69F00");
+ expect(roles.danger).toBe("#D55E00");
+ expect(roles.special).toBe("#CC79A7");
+ expect(roles.fg).toBe("#E8E8E8");
+ expect(roles.dim).toBe("#9A9A9A");
+ expect(roles.border).toBe("#6B6B6B");
+ });
+});
diff --git a/src/utils/colors.ts b/src/utils/colors.ts
index 3c04b535..652ce90a 100644
--- a/src/utils/colors.ts
+++ b/src/utils/colors.ts
@@ -1,16 +1,117 @@
+/**
+ * Shared semantic palette for CLI (ANSI) and TUI (Ink hex).
+ *
+ * Default is a dark-tuned Okabe–Ito set. Status is dual-encoded in callers
+ * (words/glyphs plus hue). --no-color / NO_COLOR still strip all SGR.
+ */
+
+export type ColorRole =
+ | "fg"
+ | "dim"
+ | "border"
+ | "accent"
+ | "success"
+ | "warning"
+ | "danger"
+ | "special";
+
+/** Dark-tuned Okabe–Ito default. */
+export const roles: Record = {
+ fg: "#E8E8E8",
+ dim: "#9A9A9A",
+ border: "#6B6B6B",
+ accent: "#56B4E9",
+ success: "#2BC48A",
+ warning: "#E69F00",
+ danger: "#D55E00",
+ special: "#CC79A7",
+};
+
+/** 16-color SGR foreground codes when COLORTERM is not truecolor/24bit. */
+const ANSI16: Record = {
+ fg: 37,
+ dim: 90,
+ border: 90,
+ accent: 36,
+ success: 32,
+ warning: 33,
+ danger: 31,
+ special: 35,
+};
+
+export function useColor(): boolean {
+ if (process.env.NO_COLOR !== undefined) return false;
+ if (globalThis.__CLI_NO_COLOR) return false;
+ if (!process.stdout.isTTY) return false;
+ return true;
+}
+
+export function useTrueColor(): boolean {
+ const ct = (process.env.COLORTERM ?? "").toLowerCase();
+ return ct === "truecolor" || ct === "24bit";
+}
+
+function hexToRgb(hex: string): [number, number, number] {
+ const h = hex.slice(1);
+ return [
+ parseInt(h.slice(0, 2), 16),
+ parseInt(h.slice(2, 4), 16),
+ parseInt(h.slice(4, 6), 16),
+ ];
+}
+
+export function paint(role: ColorRole, s: string): string {
+ if (!useColor()) return s;
+ if (useTrueColor()) {
+ const [r, g, b] = hexToRgb(roles[role]);
+ return `\x1b[38;2;${r};${g};${b}m${s}\x1b[0m`;
+ }
+ return `\x1b[${ANSI16[role]}m${s}\x1b[0m`;
+}
+
+export const ansi = {
+ bold: (s: string) => (useColor() ? `\x1b[1m${s}\x1b[0m` : s),
+ cyan: (s: string) => paint("accent", s),
+ green: (s: string) => paint("success", s),
+ yellow: (s: string) => paint("warning", s),
+ dim: (s: string) => paint("dim", s),
+ white: (s: string) => paint("fg", s),
+ red: (s: string) => paint("danger", s),
+ blue: (s: string) => paint("accent", s),
+ blueBold: (s: string) => {
+ if (!useColor()) return s;
+ if (useTrueColor()) {
+ const [r, g, b] = hexToRgb(roles.accent);
+ return `\x1b[1;38;2;${r};${g};${b}m${s}\x1b[0m`;
+ }
+ return `\x1b[36;1m${s}\x1b[0m`;
+ },
+ magenta: (s: string) => paint("special", s),
+ bgDim: (s: string) => (useColor() ? `\x1b[48;5;236m${s}\x1b[0m` : s),
+ bgRed: (s: string) =>
+ useColor() ? `\x1b[41m\x1b[37m\x1b[1m${s}\x1b[0m` : s,
+ bgYellow: (s: string) =>
+ useColor() ? `\x1b[43m\x1b[30m\x1b[1m${s}\x1b[0m` : s,
+ bgGreen: (s: string) =>
+ useColor() ? `\x1b[42m\x1b[30m\x1b[1m${s}\x1b[0m` : s,
+ bgCyan: (s: string) =>
+ useColor() ? `\x1b[46m\x1b[30m\x1b[1m${s}\x1b[0m` : s,
+};
+
+/** Ink TUI tokens. Legacy keys alias the Okabe–Ito roles. */
export const theme = {
bg: "#1a1b26",
bgAlt: "#24283b",
- fg: "#c0caf5",
- fgDim: "#565f89",
- accent: "#7aa2f7",
- accentAlt: "#bb9af7",
- green: "#9ece6a",
- red: "#f7768e",
- yellow: "#e0af68",
- cyan: "#7dcfff",
- orange: "#ff9e64",
- border: "#3b4261",
- borderFocus: "#7aa2f7",
+ fg: roles.fg,
+ fgDim: roles.dim,
+ accent: roles.accent,
+ accentAlt: roles.special,
+ green: roles.success,
+ red: roles.danger,
+ yellow: roles.warning,
+ cyan: roles.accent,
+ orange: roles.warning,
+ border: roles.border,
+ borderFocus: roles.accent,
white: "#FFFFFF",
} as const;
diff --git a/src/views/config.test.tsx b/src/views/config.test.tsx
index 1035165b..a1c5d391 100644
--- a/src/views/config.test.tsx
+++ b/src/views/config.test.tsx
@@ -49,8 +49,8 @@ describe("ConfigView", () => {
expect(frame).toContain("Tools (Enter to toggle, e to edit config file):");
expect(frame).toContain("Enter Toggle e Edit file Esc Save & close");
// First provider enabled, second disabled.
- expect(frame).toContain("✔ ON");
- expect(frame).toContain("✘ OFF");
+ expect(frame).toContain("on ");
+ expect(frame).toContain("off");
});
it("shows the project path for the selected (first) row", () => {
diff --git a/src/views/config.tsx b/src/views/config.tsx
index 08a57f48..1a572c75 100644
--- a/src/views/config.tsx
+++ b/src/views/config.tsx
@@ -9,7 +9,7 @@ function providerRow(
globalPath: string,
enabled: boolean,
): string {
- const status = enabled ? "✔ ON " : "✘ OFF";
+ const status = enabled ? "on " : "off";
const name = label.length > 14 ? label.slice(0, 14) : label;
return `${status} ${name.padEnd(15)} ${globalPath}`;
}
diff --git a/src/views/dashboard.test.tsx b/src/views/dashboard.test.tsx
index 51194a5f..db7f6ad0 100644
--- a/src/views/dashboard.test.tsx
+++ b/src/views/dashboard.test.tsx
@@ -97,9 +97,8 @@ describe("DashboardHeader", () => {
expect(frame).toContain("Symlinks:");
expect(frame).toContain("Tools:");
expect(frame).toContain("Dupes:");
- // The values column-group renders as "2 1 1 2 1" after the labels (global
- // 2, project 1, symlinks 1, tools 2, dupes 1).
- expect(frame).toContain("2 1 1 2 1");
+ // Ink wraps the stats row; dual-encoded dupes land in the values group.
+ expect(frame).toContain("2 1 1 2 ! 1");
});
it("renders the sort label with the active sort bracketed", () => {
@@ -120,6 +119,7 @@ describe("DashboardHeader", () => {
// buildSortLabel: "(s) Sort: name [version] location"
expect(frame).toContain("[version]");
expect(frame).toContain("Sort:");
+ expect(frame).not.toContain("!");
});
it("highlights the active scope tab and dims the others", () => {
diff --git a/src/views/dashboard.tsx b/src/views/dashboard.tsx
index 20846da8..80868725 100644
--- a/src/views/dashboard.tsx
+++ b/src/views/dashboard.tsx
@@ -68,11 +68,13 @@ export function DashboardHeader({
Total: {total} ({unique} unique)
- Global: {globalCount}
- Project: {projectCount}
- Symlinks: {symlinks}
- Tools: {providers}
- Dupes: {duplicateCount}
+ Global: {globalCount}
+ Project: {projectCount}
+ Symlinks: {symlinks}
+ Tools: {providers}
+ 0 ? theme.yellow : theme.fgDim}>
+ Dupes: {duplicateCount > 0 ? `!\u00a0${duplicateCount}` : duplicateCount}
+
│
{buildSortLabel(sort)}
From b1e716b7d5c02f4cc8e239a09263c936a264ddaa Mon Sep 17 00:00:00 2001
From: AgileInnov8tor <127654909+AgileInnov8tor@users.noreply.github.com>
Date: Sat, 22 Aug 2026 19:36:40 +0700
Subject: [PATCH 2/2] ux: quiet TUI selection and restore labeled stats chrome
Inverse and neon accent bars read poorly in Warp. Paint a dark canvas,
use a navy wash for the cursor, keep Okabe-Ito hues on labeled stats and
effort, and mark the active scope with [brackets] instead of a filled chip.
---
src/index.tsx | 7 ++++++-
src/utils/colors.ts | 6 ++++++
src/views/config.tsx | 13 ++++++++-----
src/views/dashboard.tsx | 24 +++++++++++-------------
src/views/duplicates.tsx | 20 +++++++++++++-------
src/views/skill-list.tsx | 40 +++++++++++++++++++++++++++++++++-------
6 files changed, 77 insertions(+), 33 deletions(-)
diff --git a/src/index.tsx b/src/index.tsx
index 3c5bc34e..1347f3a6 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -334,7 +334,12 @@ export function App({ initialConfig }: AppProps) {
const visibleListRows = Math.max(5, termHeight - chromeRows - 4);
return (
-
+
{view === "dashboard" && (
<>
{scanning && !hasScanned && (
diff --git a/src/utils/colors.ts b/src/utils/colors.ts
index 652ce90a..e7eb2a09 100644
--- a/src/utils/colors.ts
+++ b/src/utils/colors.ts
@@ -115,3 +115,9 @@ export const theme = {
borderFocus: roles.accent,
white: "#FFFFFF",
} as const;
+
+/** Selected-row style: muted navy wash, light text. Never inverse, never a neon bar. */
+export const selectedFill = {
+ color: theme.fg,
+ backgroundColor: theme.bgAlt,
+} as const;
diff --git a/src/views/config.tsx b/src/views/config.tsx
index 1a572c75..4bfbc2a5 100644
--- a/src/views/config.tsx
+++ b/src/views/config.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { Box, Text, useInput } from "ink";
-import { theme } from "../utils/colors";
+import { selectedFill, theme } from "../utils/colors";
import { getConfigPath } from "../config";
import type { AppConfig } from "../utils/types";
@@ -88,11 +88,14 @@ export function ConfigView({ config, onClose, onOpenEditor }: ConfigProps) {
return (
- {isSelected ? "❯ " : " "}
- {row}
+
+ {isSelected ? "❯ " : " "}
+
+ {row}
{isSelected && (
Project: {p.project}
diff --git a/src/views/dashboard.tsx b/src/views/dashboard.tsx
index 80868725..1a671b0e 100644
--- a/src/views/dashboard.tsx
+++ b/src/views/dashboard.tsx
@@ -54,8 +54,9 @@ export function DashboardHeader({
- agent-skill-manager
+ asm
+ agent-skill-manager
Total: {total} ({unique} unique)
- Global: {globalCount}
- Project: {projectCount}
- Symlinks: {symlinks}
- Tools: {providers}
- 0 ? theme.yellow : theme.fgDim}>
+ Global: {globalCount}
+ Project: {projectCount}
+ Symlinks: {symlinks}
+ Tools: {providers}
+ 0 ? theme.orange : theme.fgDim}>
Dupes: {duplicateCount > 0 ? `!\u00a0${duplicateCount}` : duplicateCount}
│
@@ -112,13 +113,10 @@ export function DashboardHeader({
function ScopeTab({ label, active }: { label: string; active: boolean }) {
return (
-
- {" "}
- {label}{" "}
+
+ {active ? "[" : " "}
+ {label}
+ {active ? "]" : " "}
);
}
diff --git a/src/views/duplicates.tsx b/src/views/duplicates.tsx
index 64e67d45..e368a7cd 100644
--- a/src/views/duplicates.tsx
+++ b/src/views/duplicates.tsx
@@ -1,6 +1,6 @@
import React, { useMemo, useState } from "react";
import { Box, Text, useInput } from "ink";
-import { theme } from "../utils/colors";
+import { selectedFill, theme } from "../utils/colors";
import { sortInstancesForKeep, reasonLabel } from "../auditor";
import type { AuditReport, DuplicateGroup, SkillInfo } from "../utils/types";
@@ -165,8 +165,10 @@ function GroupsPhase({
return (
{isSelected ? "❯ " : " "}
{label}
@@ -207,8 +209,10 @@ function InstancesPhase({
return (
{isSelected ? "❯ " : " "}
{checked} {s.providerLabel}/{s.scope} - {s.path}
@@ -217,8 +221,10 @@ function InstancesPhase({
);
})}
{cursor === sorted.length ? "❯ " : " "}
{actionLabel}
diff --git a/src/views/skill-list.tsx b/src/views/skill-list.tsx
index d20d3b6e..0cd4db50 100644
--- a/src/views/skill-list.tsx
+++ b/src/views/skill-list.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { Box, Text } from "ink";
-import { theme } from "../utils/colors";
+import { selectedFill, theme } from "../utils/colors";
import type { SkillInfo } from "../utils/types";
import { formatTokenCount } from "../utils/token-count";
import { formatInvocability } from "../utils/frontmatter";
@@ -27,7 +27,7 @@ function formatSkillRow(
index: number,
skill: SkillInfo,
descWidth: number,
-): string {
+): { before: string; effortCell: string; after: string; full: string } {
const idx = String(index).padStart(3);
const prefix = skill.isSymlink ? "~ " : " ";
const nameMax = 24 - prefix.length;
@@ -53,7 +53,30 @@ function formatSkillRow(
const type = skill.isSymlink ? "→link" : " dir ";
const desc =
descWidth > 0 ? " " + (skill.description || "").slice(0, descWidth) : "";
- return `${idx} ${name.padEnd(24)} ${ver.padEnd(8)} ${creator.padEnd(11)} ${effort.padEnd(7)} ${invoke.padEnd(6)} ${tokens.padEnd(6)} ${prov.padEnd(12)} ${scope.padEnd(7)} ${type.padEnd(5)}${desc}`;
+ const before = `${idx} ${name.padEnd(24)} ${ver.padEnd(8)} ${creator.padEnd(11)} `;
+ const effortCell = effort.padEnd(7);
+ const after = ` ${invoke.padEnd(6)} ${tokens.padEnd(6)} ${prov.padEnd(12)} ${scope.padEnd(7)} ${type.padEnd(5)}${desc}`;
+ return {
+ before,
+ effortCell,
+ after,
+ full: `${before}${effortCell}${after}`,
+ };
+}
+
+function effortColor(effort: string | undefined): string {
+ switch ((effort || "").toLowerCase()) {
+ case "low":
+ return theme.green;
+ case "medium":
+ return theme.yellow;
+ case "high":
+ return theme.red;
+ case "max":
+ return theme.accentAlt;
+ default:
+ return theme.fgDim;
+ }
}
export interface SkillListProps {
@@ -112,11 +135,14 @@ export function SkillListView({
return (
- {prefix}
- {row}
+ {prefix}
+ {row.before}
+ {row.effortCell}
+ {row.after}
);
})}