diff --git a/CHANGELOG.md b/CHANGELOG.md index 24fe898c5..d2086f57a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ Docs: https://docs.clawd.bot - CLI: render auth probe results as a table in `clawdbot models status`. - CLI: suppress probe-only embedded logs unless `--verbose` is set. - CLI: move auth probe errors below the table to reduce wrapping. +- CLI: prevent ANSI color bleed when table cells wrap. +- CLI: explain when auth profiles are excluded by auth.order in probe details. - Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla. - TUI: render Gateway slash-command replies as system output (for example, `/context`). - Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla. diff --git a/src/commands/models/list.probe.ts b/src/commands/models/list.probe.ts index fbd172b57..fb7d23223 100644 --- a/src/commands/models/list.probe.ts +++ b/src/commands/models/list.probe.ts @@ -6,6 +6,7 @@ import { ensureAuthProfileStore, listProfilesForProvider, resolveAuthProfileDisplayLabel, + resolveAuthProfileOrder, } from "../../agents/auth-profiles.js"; import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js"; import { describeFailoverError } from "../../agents/failover-error.js"; @@ -143,6 +144,25 @@ function buildProbeTargets(params: { }); const profileIds = listProfilesForProvider(store, providerKey); + const explicitOrder = (() => { + const order = store.order; + if (order) { + for (const [key, value] of Object.entries(order)) { + if (normalizeProviderId(key) === providerKey) return value; + } + } + const cfgOrder = cfg?.auth?.order; + if (cfgOrder) { + for (const [key, value] of Object.entries(cfgOrder)) { + if (normalizeProviderId(key) === providerKey) return value; + } + } + return undefined; + })(); + const allowedProfiles = + explicitOrder && explicitOrder.length > 0 + ? new Set(resolveAuthProfileOrder({ cfg, store, provider: providerKey })) + : null; const filteredProfiles = profileFilter.size ? profileIds.filter((id) => profileFilter.has(id)) : profileIds; @@ -152,6 +172,32 @@ function buildProbeTargets(params: { const profile = store.profiles[profileId]; const mode = profile?.type; const label = resolveAuthProfileDisplayLabel({ cfg, store, profileId }); + if (explicitOrder && !explicitOrder.includes(profileId)) { + results.push({ + provider: providerKey, + model: model ? `${model.provider}/${model.model}` : undefined, + profileId, + label, + source: "profile", + mode, + status: "unknown", + error: "Excluded by auth.order for this provider.", + }); + continue; + } + if (allowedProfiles && !allowedProfiles.has(profileId)) { + results.push({ + provider: providerKey, + model: model ? `${model.provider}/${model.model}` : undefined, + profileId, + label, + source: "profile", + mode, + status: "unknown", + error: "Auth profile credentials are missing or expired.", + }); + continue; + } if (!model) { results.push({ provider: providerKey, diff --git a/src/terminal/table.test.ts b/src/terminal/table.test.ts index c2108accb..1e7b24b76 100644 --- a/src/terminal/table.test.ts +++ b/src/terminal/table.test.ts @@ -85,6 +85,31 @@ describe("renderTable", () => { } }); + it("resets ANSI styling on wrapped lines", () => { + const reset = "\x1b[0m"; + const out = renderTable({ + width: 24, + columns: [ + { key: "K", header: "K", minWidth: 3 }, + { key: "V", header: "V", flex: true, minWidth: 10 }, + ], + rows: [ + { + K: "X", + V: `\x1b[31m${"a".repeat(80)}${reset}`, + }, + ], + }); + + const lines = out.split("\n").filter((line) => line.includes("a")); + for (const line of lines) { + const resetIndex = line.lastIndexOf(reset); + const lastSep = line.lastIndexOf("│"); + expect(resetIndex).toBeGreaterThan(-1); + expect(lastSep).toBeGreaterThan(resetIndex); + } + }); + it("respects explicit newlines in cell values", () => { const out = renderTable({ width: 48, diff --git a/src/terminal/table.ts b/src/terminal/table.ts index f0ea1cb02..5a735a749 100644 --- a/src/terminal/table.ts +++ b/src/terminal/table.ts @@ -91,6 +91,27 @@ function wrapLine(text: string, width: number): string[] { i += ch.length; } + const firstCharIndex = tokens.findIndex((t) => t.kind === "char"); + if (firstCharIndex < 0) return [text]; + let lastCharIndex = -1; + for (let i = tokens.length - 1; i >= 0; i -= 1) { + if (tokens[i]?.kind === "char") { + lastCharIndex = i; + break; + } + } + const prefixAnsi = tokens + .slice(0, firstCharIndex) + .filter((t) => t.kind === "ansi") + .map((t) => t.value) + .join(""); + const suffixAnsi = tokens + .slice(lastCharIndex + 1) + .filter((t) => t.kind === "ansi") + .map((t) => t.value) + .join(""); + const coreTokens = tokens.slice(firstCharIndex, lastCharIndex + 1); + const lines: string[] = []; const isBreakChar = (ch: string) => ch === " " || ch === "\t" || ch === "/" || ch === "-" || ch === "_" || ch === "."; @@ -136,7 +157,7 @@ function wrapLine(text: string, width: number): string[] { lastBreakIndex = null; }; - for (const token of tokens) { + for (const token of coreTokens) { if (token.kind === "ansi") { buf.push(token); continue; @@ -162,7 +183,12 @@ function wrapLine(text: string, width: number): string[] { } flushAt(buf.length); - return lines.length ? lines : [""]; + if (!lines.length) return [""]; + if (!prefixAnsi && !suffixAnsi) return lines; + return lines.map((line) => { + if (!line) return line; + return `${prefixAnsi}${line}${suffixAnsi}`; + }); } function normalizeWidth(n: number | undefined): number | undefined {