惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
月光博客
月光博客
MyScale Blog
MyScale Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
爱范儿
爱范儿
P
Proofpoint News Feed
人人都是产品经理
人人都是产品经理
Last Week in AI
Last Week in AI
罗磊的独立博客
G
Google Developers Blog
Y
Y Combinator Blog
博客园 - 【当耐特】
WordPress大学
WordPress大学
大猫的无限游戏
大猫的无限游戏
博客园 - 叶小钗
J
Java Code Geeks
酷 壳 – CoolShell
酷 壳 – CoolShell
V
Visual Studio Blog
美团技术团队
宝玉的分享
宝玉的分享
Jina AI
Jina AI
小众软件
小众软件
T
Tailwind CSS Blog
A
About on SuperTechFans

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(cli): preserve multiline table colors · openclaw/open...
vincentkoc · 2026-05-14 · via Recent Commits to openclaw:main

@@ -70,9 +70,10 @@ function wrapLine(text: string, width: number): string[] {

7070

}

71717272

// ANSI-aware wrapping: never split inside ANSI SGR/OSC-8 sequences.

73-

// We don't attempt to re-open styling per line; terminals keep SGR state

74-

// across newlines, so as long as we don't corrupt escape sequences we're safe.

73+

// Table cells are padded and bordered per physical line, so wrapped lines

74+

// must not leak styling into padding while the next continuation keeps it.

7575

const ESC = "\u001b";

76+

const SGR_RESET = `${ESC}[0m`;

76777778

type Token = { kind: "ansi" | "char"; value: string };

7879

const tokens: Token[] = [];

@@ -170,9 +171,140 @@ function wrapLine(text: string, width: number): string[] {

170171

const bufVisibleWidth = (slice: Token[]) =>

171172

slice.reduce((acc, t) => acc + (t.kind === "char" ? visibleWidth(t.value) : 0), 0);

172173174+

const parseSgrParams = (value: string): number[] | null => {

175+

if (!value.startsWith(`${ESC}[`) || !value.endsWith("m")) {

176+

return null;

177+

}

178+

const raw = value.slice(2, -1);

179+

if (!raw) {

180+

return [0];

181+

}

182+

const params = raw.split(";").map((part) => (part === "" ? 0 : Number(part)));

183+

return params.every((param) => Number.isInteger(param)) ? params : null;

184+

};

185+186+

const activeSgrAfter = (tokens: Token[]) => {

187+

type SgrCategory =

188+

| "background"

189+

| "blink"

190+

| "conceal"

191+

| "foreground"

192+

| "intensity"

193+

| "inverse"

194+

| "italic"

195+

| "strike"

196+

| "underline";

197+

const active: Array<{ value: string; categories: Set<SgrCategory> }> = [];

198+

const resetCategoriesFor = (params: number[]) => {

199+

const categories = new Set<SgrCategory>();

200+

for (const param of params) {

201+

if (param === 22) {

202+

categories.add("intensity");

203+

} else if (param === 23) {

204+

categories.add("italic");

205+

} else if (param === 24) {

206+

categories.add("underline");

207+

} else if (param === 25) {

208+

categories.add("blink");

209+

} else if (param === 27) {

210+

categories.add("inverse");

211+

} else if (param === 28) {

212+

categories.add("conceal");

213+

} else if (param === 29) {

214+

categories.add("strike");

215+

} else if (param === 39) {

216+

categories.add("foreground");

217+

} else if (param === 49) {

218+

categories.add("background");

219+

}

220+

}

221+

return categories;

222+

};

223+

const activeCategoriesFor = (params: number[]) => {

224+

const categories = new Set<SgrCategory>();

225+

for (let i = 0; i < params.length; i += 1) {

226+

const param = params[i] ?? 0;

227+

if (param === 1 || param === 2) {

228+

categories.add("intensity");

229+

} else if (param === 3) {

230+

categories.add("italic");

231+

} else if (param === 4) {

232+

categories.add("underline");

233+

} else if (param === 5 || param === 6) {

234+

categories.add("blink");

235+

} else if (param === 7) {

236+

categories.add("inverse");

237+

} else if (param === 8) {

238+

categories.add("conceal");

239+

} else if (param === 9) {

240+

categories.add("strike");

241+

} else if ((param >= 30 && param <= 37) || (param >= 90 && param <= 97)) {

242+

categories.add("foreground");

243+

} else if (param === 38) {

244+

categories.add("foreground");

245+

if (params[i + 1] === 2) {

246+

i += 4;

247+

} else if (params[i + 1] === 5) {

248+

i += 2;

249+

}

250+

} else if ((param >= 40 && param <= 47) || (param >= 100 && param <= 107)) {

251+

categories.add("background");

252+

} else if (param === 48) {

253+

categories.add("background");

254+

if (params[i + 1] === 2) {

255+

i += 4;

256+

} else if (params[i + 1] === 5) {

257+

i += 2;

258+

}

259+

}

260+

}

261+

return categories;

262+

};

263+

const intersects = (left: Set<SgrCategory>, right: Set<SgrCategory>) => {

264+

for (const value of left) {

265+

if (right.has(value)) {

266+

return true;

267+

}

268+

}

269+

return false;

270+

};

271+

for (const token of tokens) {

272+

if (token.kind !== "ansi") {

273+

continue;

274+

}

275+

const params = parseSgrParams(token.value);

276+

if (!params) {

277+

continue;

278+

}

279+

if (params.includes(0)) {

280+

active.length = 0;

281+

}

282+

const resetCategories = resetCategoriesFor(params);

283+

if (resetCategories.size > 0) {

284+

for (let i = active.length - 1; i >= 0; i -= 1) {

285+

const entry = active[i];

286+

if (entry && intersects(entry.categories, resetCategories)) {

287+

active.splice(i, 1);

288+

}

289+

}

290+

}

291+

const activeCategories = activeCategoriesFor(params);

292+

if (activeCategories.size > 0) {

293+

for (let i = active.length - 1; i >= 0; i -= 1) {

294+

const entry = active[i];

295+

if (entry && intersects(entry.categories, activeCategories)) {

296+

active.splice(i, 1);

297+

}

298+

}

299+

active.push({ value: token.value, categories: activeCategories });

300+

}

301+

}

302+

return active.map((entry) => entry.value).join("");

303+

};

304+173305

const pushLine = (value: string) => {

174306

const cleaned = value.replace(/\s+$/, "");

175-

if (cleaned.trim().length === 0) {

307+

if (visibleWidth(cleaned) === 0) {

176308

return;

177309

}

178310

lines.push(cleaned);

@@ -197,17 +329,25 @@ function wrapLine(text: string, width: number): string[] {

197329

return;

198330

}

199331

if (breakAt == null || breakAt <= 0) {

200-

pushLine(bufToString());

332+

const activeSgr = activeSgrAfter(buf);

333+

pushLine(activeSgr ? `${bufToString()}${SGR_RESET}` : bufToString());

201334

buf.length = 0;

335+

if (activeSgr) {

336+

buf.push({ kind: "ansi", value: activeSgr });

337+

}

202338

bufVisible = 0;

203339

lastBreakIndex = null;

204340

return;

205341

}

206342207343

const left = buf.slice(0, breakAt);

208344

const rest = buf.slice(breakAt);

209-

pushLine(bufToString(left));

345+

const activeSgr = activeSgrAfter(left);

346+

pushLine(activeSgr ? `${bufToString(left)}${SGR_RESET}` : bufToString(left));

210347

trimLeadingSpaces(rest);

348+

if (activeSgr) {

349+

rest.unshift({ kind: "ansi", value: activeSgr });

350+

}

211351212352

buf.length = 0;

213353

buf.push(...rest);