diff --git a/assets/sai.css b/assets/sai.css
index e985090..c2f5b1d 100644
--- a/assets/sai.css
+++ b/assets/sai.css
@@ -919,3 +919,35 @@ img.branch {
background-color: #f44336; /* red */
height: 10px;
}
+
+/* ANSI Color Support */
+.ansi-bold { font-weight: bold; }
+.ansi-underline { text-decoration: underline; }
+
+.ansi-fg-black { color: #000; }
+.ansi-fg-red { color: #A00; }
+.ansi-fg-green { color: #0A0; }
+.ansi-fg-yellow { color: #A50; }
+.ansi-fg-blue { color: #00A; }
+.ansi-fg-magenta { color: #A0A; }
+.ansi-fg-cyan { color: #0AA; }
+.ansi-fg-white { color: #AAA; }
+
+/* Bright colors for bold text */
+.ansi-bold .ansi-fg-black { color: #555; }
+.ansi-bold .ansi-fg-red { color: #F55; }
+.ansi-bold .ansi-fg-green { color: #5F5; }
+.ansi-bold .ansi-fg-yellow { color: #FF5; }
+.ansi-bold .ansi-fg-blue { color: #55F; }
+.ansi-bold .ansi-fg-magenta { color: #F5F; }
+.ansi-bold .ansi-fg-cyan { color: #5FF; }
+.ansi-bold .ansi-fg-white { color: #FFF; }
+
+.ansi-bg-black { background-color: #000; }
+.ansi-bg-red { background-color: #A00; }
+.ansi-bg-green { background-color: #0A0; }
+.ansi-bg-yellow { background-color: #A50; }
+.ansi-bg-blue { background-color: #00A; }
+.ansi-bg-magenta { background-color: #A0A; }
+.ansi-bg-cyan { background-color: #0AA; }
+.ansi-bg-white { background-color: #AAA; }
diff --git a/assets/sai.js b/assets/sai.js
index c6681f4..8bc7959 100644
--- a/assets/sai.js
+++ b/assets/sai.js
@@ -395,6 +395,7 @@ var lang_zhs = "{" +
"}}";
var logs = "", redpend = 0, gitohashi_integ = 0, authd = 0, exptimer, auth_user = "",
+ logAnsiState = {},
ongoing_task_activities = {}, last_log_timestamp = 0;
function update_task_activities() {
@@ -439,6 +440,59 @@ function humanize(s)
return s;
}
+function ansiToHtml(text, state) {
+ const classMap = {
+ '1': 'ansi-bold', '4': 'ansi-underline',
+ '30': 'ansi-fg-black', '31': 'ansi-fg-red', '32': 'ansi-fg-green', '33': 'ansi-fg-yellow', '34': 'ansi-fg-blue', '35': 'ansi-fg-magenta', '36': 'ansi-fg-cyan', '37': 'ansi-fg-white',
+ '40': 'ansi-bg-black', '41': 'ansi-bg-red', '42': 'ansi-bg-green', '43': 'ansi-bg-yellow', '44': 'ansi-bg-blue', '45': 'ansi-bg-magenta', '46': 'ansi-bg-cyan', '47': 'ansi-bg-white',
+ };
+
+ // Ensure state is a valid object
+ state = state || {};
+ let currentClasses = new Set(state.classes || []);
+
+ const parts = text.split(/(\u001b\[[\d;]*m)/);
+ let html = '';
+
+ for (const part of parts) {
+ if (!part) continue;
+
+ if (part.startsWith('\u001b[')) { // It's an ANSI code
+ const codes = part.substring(2, part.length - 1).split(';');
+
+ if (codes.length === 1 && (codes[0] === '0' || codes[0] === '')) {
+ // Reset
+ currentClasses.clear();
+ } else {
+ for (const code of codes) {
+ if (classMap[code]) {
+ // Handle foreground/background colors: remove old before adding new
+ if (code >= 30 && code <= 37) {
+ currentClasses.forEach(c => { if (c.startsWith('ansi-fg-')) currentClasses.delete(c); });
+ }
+ if (code >= 40 && code <= 47) {
+ currentClasses.forEach(c => { if (c.startsWith('ansi-bg-')) currentClasses.delete(c); });
+ }
+ currentClasses.add(classMap[code]);
+ }
+ }
+ }
+ } else { // It's plain text
+ const sanitizedPart = hsanitize(part);
+ if (currentClasses.size > 0) {
+ html += `<span class="${Array.from(currentClasses).join(' ')}">${sanitizedPart}</span>`;
+ } else {
+ html += sanitizedPart;
+ }
+ }
+ }
+
+ return {
+ html: html,
+ newState: { classes: Array.from(currentClasses) }
+ };
+}
+
function hsanitize(s)
{
var table = {
@@ -1523,6 +1577,7 @@ function ws_open_sai()
document.getElementById("dlogst").innerHTML = "";
document.getElementById("logs").innerHTML = "";
lines = times = logs = "";
+ logAnsiState = {};
tfirst = 0;
lli = 1;
last_log_timestamp = 0;
@@ -1648,8 +1703,11 @@ function ws_open_sai()
case "com-warmcat-sai-logs":
try {
- var s1 = decodeURIComponent(escape(atob(jso.log))), s = hsanitize(s1), li,
- en = "", yo, dh, ce, tn = "";
+ var s1 = decodeURIComponent(escape(atob(jso.log))),
+ ansiResult = ansiToHtml(s1, logAnsiState),
+ s = ansiResult.html, li,
+ en = "", yo, dh, ce, tn = "";
+ logAnsiState = ansiResult.newState;
} catch (e) {
break;
}