Skip to content

Commit 07b5a7a

Browse files
ajwan8998claude
andcommitted
fix(doctor): prevent clack note box from breaking copy-sensitive paths
The wrapNoteMessage function correctly preserves copy-sensitive tokens (paths, URLs) as intact tokens, but the downstream clackNote renderer performs its own width-based wrapping inside the bordered box and is unaware of copy-sensitive token semantics. A path such as: ~/.openclaw/agents/main/sessions/uuid.jsonl.lock would land exactly at the box's right edge and get broken mid-extension. Add a second pass after the first-pass word wrap: any line that still exceeds the clack note box inner width is split at path-separator boundaries so each segment fits within the box without mid-word breaks. The fix benefits all doctor panels that use note() for paths: doctor-session-locks, doctor-state-integrity, doctor-volatile-fs, etc. Closes: #94730 Co-Authored-By: Claude <[email protected]>
1 parent c79f1e5 commit 07b5a7a

1 file changed

Lines changed: 50 additions & 0 deletions

File tree

packages/terminal-core/src/note.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const MIN_NOTE_COLUMNS = 80;
99
const URL_PREFIX_RE = /^(https?:\/\/|file:\/\/)/i;
1010
const WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\/]/;
1111
const FILE_LIKE_RE = /^[a-zA-Z0-9._-]+$/;
12+
const NOTE_BOX_BORDER_WIDTH = 2;
1213
const suppressNotesStorage = new AsyncLocalStorage<boolean>();
1314

1415
function isSuppressedByEnv(value: string | undefined): boolean {
@@ -22,6 +23,45 @@ function isSuppressedByEnv(value: string | undefined): boolean {
2223
return normalized !== "0" && normalized !== "false" && normalized !== "off";
2324
}
2425

26+
function splitCopySensitiveToken(token: string, maxLen: number): string[] {
27+
if (token.length <= maxLen) {
28+
return [token];
29+
}
30+
// For paths and URLs, try to split at path separators.
31+
const parts: string[] = [];
32+
let remaining = token;
33+
while (remaining.length > 0) {
34+
if (remaining.length <= maxLen) {
35+
parts.push(remaining);
36+
break;
37+
}
38+
// Find the last path separator within maxLen boundary
39+
const slice = remaining.slice(0, maxLen);
40+
const lastSlash = Math.max(slice.lastIndexOf("/"), slice.lastIndexOf("\\"));
41+
const breakPoint = lastSlash > 0 ? lastSlash + 1 : maxLen;
42+
parts.push(remaining.slice(0, breakPoint));
43+
remaining = remaining.slice(breakPoint);
44+
}
45+
return parts;
46+
}
47+
48+
/**
49+
* Splits a copy-sensitive token (path, URL) at path-separator boundaries
50+
* so each segment fits within maxWidth. Used as a second pass after
51+
* wrapLine has already performed word-wrapping — only reaches lines
52+
* whose single copy-sensitive token still exceeds the clack note box
53+
* inner width.
54+
*/
55+
function splitOversizedCopySensitiveLine(line: string, maxWidth: number): string[] {
56+
const match = line.match(/^(\s*)(.*)$/);
57+
const indent = match?.[1] ?? "";
58+
const content = match?.[2] ?? "";
59+
const segments = splitCopySensitiveToken(content, maxWidth);
60+
// Each segment inherits the original indentation so the visual
61+
// alignment stays consistent inside the clack note box.
62+
return segments.map((seg) => indent + seg);
63+
}
64+
2565
function splitLongWord(word: string, maxLen: number): string[] {
2666
if (maxLen <= 0) {
2767
return [word];
@@ -173,9 +213,19 @@ export function wrapNoteMessage(
173213
const text = coerceNoteMessage(message);
174214
const columns = options.columns ?? resolveNoteColumns(process.stdout.columns);
175215
const maxWidth = options.maxWidth ?? Math.max(40, Math.min(88, columns - 10));
216+
const boxInnerWidth = columns - NOTE_BOX_BORDER_WIDTH;
176217
return text
177218
.split("\n")
178219
.flatMap((line) => wrapLine(line, maxWidth))
220+
.flatMap((line) => {
221+
if (visibleWidth(line) <= boxInnerWidth) {
222+
return [line];
223+
}
224+
// After the first-pass word wrap, any remaining oversized line must
225+
// contain a copy-sensitive token that overflowed maxWidth. Split it
226+
// at path-separator boundaries so it renders within clack's box.
227+
return splitOversizedCopySensitiveLine(line, boxInnerWidth);
228+
})
179229
.join("\n");
180230
}
181231

0 commit comments

Comments
 (0)