@liguoshuai/pi-web-chat 2.15.4 → 2.16.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/public/app.js +128 -24
- package/public/index.html +8 -0
- package/public/markdown.js +18 -3
- package/public/style.css +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liguoshuai/pi-web-chat",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.5",
|
|
4
4
|
"description": "A ChatGPT/Gemini-style web UI for the pi coding agent, powered by pi's RPC mode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"express": "^4.21.2",
|
|
44
44
|
"ws": "^8.18.0",
|
|
45
|
-
"@liguoshuai/pi-chat-protocol": "2.
|
|
46
|
-
"@liguoshuai/pi-chat-server": "2.
|
|
45
|
+
"@liguoshuai/pi-chat-protocol": "2.16.5",
|
|
46
|
+
"@liguoshuai/pi-chat-server": "2.16.5"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "node --check server.js && node --check bin/pi-web-chat.js && node --check public/markdown.js && node --check public/app.js",
|
package/public/app.js
CHANGED
|
@@ -81,6 +81,7 @@ const state = {
|
|
|
81
81
|
attachedImages: [], // Array of { data: string (base64), mimeType: string, url: string }
|
|
82
82
|
turnStartedAt: null,
|
|
83
83
|
streamingMsgDurationEl: null,
|
|
84
|
+
lastSessions: [],
|
|
84
85
|
};
|
|
85
86
|
|
|
86
87
|
let toastTimer = null;
|
|
@@ -308,9 +309,11 @@ const makeCopyIconSvg = () => el("svg", {
|
|
|
308
309
|
stroke: "currentColor",
|
|
309
310
|
"stroke-width": "2",
|
|
310
311
|
"stroke-linecap": "round",
|
|
311
|
-
"stroke-linejoin": "round"
|
|
312
|
-
|
|
313
|
-
})
|
|
312
|
+
"stroke-linejoin": "round"
|
|
313
|
+
}, [
|
|
314
|
+
el("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
|
|
315
|
+
el("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
|
|
316
|
+
]);
|
|
314
317
|
|
|
315
318
|
// ---- Message Time Formatter ----
|
|
316
319
|
function formatMessageTime(ts) {
|
|
@@ -396,10 +399,14 @@ async function refreshSessions() {
|
|
|
396
399
|
try {
|
|
397
400
|
const res = await authFetch(`${API}/api/sessions?cwd=${encodeURIComponent(cwd)}`);
|
|
398
401
|
const data = await res.json();
|
|
399
|
-
|
|
402
|
+
state.lastSessions = data.sessions || [];
|
|
403
|
+
renderSidebar(state.lastSessions);
|
|
400
404
|
} catch (err) {
|
|
401
405
|
console.warn("refreshSessions error:", err);
|
|
402
|
-
|
|
406
|
+
// Preserve existing sessions list rather than blanking out the sidebar on error
|
|
407
|
+
if (state.lastSessions && state.lastSessions.length > 0) {
|
|
408
|
+
renderSidebar(state.lastSessions);
|
|
409
|
+
}
|
|
403
410
|
}
|
|
404
411
|
}
|
|
405
412
|
|
|
@@ -527,6 +534,7 @@ function startNewSession() {
|
|
|
527
534
|
window.history.replaceState({}, "", window.location.pathname);
|
|
528
535
|
} catch {}
|
|
529
536
|
$("#topSessionName").textContent = "新对话";
|
|
537
|
+
updatePageTitle(null);
|
|
530
538
|
if (wasStreaming) {
|
|
531
539
|
showToast("前一个会话已转入后台继续运行");
|
|
532
540
|
}
|
|
@@ -534,8 +542,8 @@ function startNewSession() {
|
|
|
534
542
|
if (window.innerWidth <= 768) closeSidebar();
|
|
535
543
|
|
|
536
544
|
connectWs({ explicitNewSession: true }); // no session -> pi creates a new one
|
|
537
|
-
//
|
|
538
|
-
renderSidebar([]);
|
|
545
|
+
// Keep existing sessions in sidebar and immediately display new draft item
|
|
546
|
+
renderSidebar(state.lastSessions || []);
|
|
539
547
|
refreshSessions();
|
|
540
548
|
}
|
|
541
549
|
|
|
@@ -553,7 +561,7 @@ async function deleteSession(file, title) {
|
|
|
553
561
|
return;
|
|
554
562
|
}
|
|
555
563
|
showToast("会话已删除");
|
|
556
|
-
if (state.currentSessionFile
|
|
564
|
+
if (sameSession(state.currentSessionFile, file)) {
|
|
557
565
|
startNewSession();
|
|
558
566
|
} else {
|
|
559
567
|
await refreshSessions();
|
|
@@ -586,6 +594,12 @@ function initMobileToolbarFab() {
|
|
|
586
594
|
} else {
|
|
587
595
|
fab.classList.remove("visible");
|
|
588
596
|
}
|
|
597
|
+
const distanceFromBottom = chat.scrollHeight - chat.scrollTop - chat.clientHeight;
|
|
598
|
+
if (distanceFromBottom <= 80) {
|
|
599
|
+
userScrolledUp = false;
|
|
600
|
+
} else if (distanceFromBottom > 150) {
|
|
601
|
+
userScrolledUp = true;
|
|
602
|
+
}
|
|
589
603
|
};
|
|
590
604
|
|
|
591
605
|
chat.addEventListener("scroll", onScroll);
|
|
@@ -616,6 +630,7 @@ async function syncSessionHistory(file, force = false) {
|
|
|
616
630
|
|
|
617
631
|
const topName = data.sessionName || data.firstUser || "新对话";
|
|
618
632
|
$("#topSessionName").textContent = topName;
|
|
633
|
+
updatePageTitle(topName);
|
|
619
634
|
|
|
620
635
|
// Only overwrite chat if not actively backfilling
|
|
621
636
|
if (!state.isBackfilling && (force || !state.streaming)) {
|
|
@@ -625,7 +640,8 @@ async function syncSessionHistory(file, force = false) {
|
|
|
625
640
|
for (const m of msgs) {
|
|
626
641
|
appendMessageNode(m.role, m);
|
|
627
642
|
}
|
|
628
|
-
|
|
643
|
+
userScrolledUp = false;
|
|
644
|
+
scrollBottom(true);
|
|
629
645
|
refreshSessions();
|
|
630
646
|
}
|
|
631
647
|
} catch (e) {
|
|
@@ -681,16 +697,39 @@ function reconstructFromEntries(entries, timing = null) {
|
|
|
681
697
|
}
|
|
682
698
|
}
|
|
683
699
|
|
|
700
|
+
// Precompute which entry indices are the last assistant message in their turn.
|
|
701
|
+
// A turn is delimited by user messages; the last assistant message before the
|
|
702
|
+
// next user message (or end of entries) is the last in its turn.
|
|
703
|
+
// We only show the total turn duration on this final message, not on every
|
|
704
|
+
// intermediate assistant message (e.g. tool-call messages within the same turn).
|
|
705
|
+
const lastAssistantInTurn = new Set();
|
|
706
|
+
let lastAssistantIdx = null;
|
|
707
|
+
for (let i = 0; i < entries.length; i++) {
|
|
708
|
+
const e = entries[i];
|
|
709
|
+
if (e.type !== "message" || !e.message) continue;
|
|
710
|
+
if (e.message.role === "assistant") {
|
|
711
|
+
lastAssistantIdx = i;
|
|
712
|
+
} else if (e.message.role === "user" && lastAssistantIdx != null) {
|
|
713
|
+
lastAssistantInTurn.add(lastAssistantIdx);
|
|
714
|
+
lastAssistantIdx = null;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (lastAssistantIdx != null) lastAssistantInTurn.add(lastAssistantIdx);
|
|
718
|
+
|
|
684
719
|
const out = [];
|
|
685
720
|
let lastUserTs = null;
|
|
686
|
-
let
|
|
687
|
-
|
|
721
|
+
let turnIndex = -1; // incremented per user message — aligns with timing[] (one entry per turn)
|
|
722
|
+
let thinkingIdx = 0; // accumulated across assistant messages within the same turn
|
|
723
|
+
for (let i = 0; i < entries.length; i++) {
|
|
724
|
+
const e = entries[i];
|
|
688
725
|
if (e.type !== "message") continue;
|
|
689
726
|
const m = e.message;
|
|
690
727
|
if (!m || m.role === "bashExecution") continue;
|
|
691
728
|
const msgTs = parseEntryTimestamp(m.timestamp || e.timestamp);
|
|
692
729
|
if (m.role === "user") {
|
|
693
730
|
lastUserTs = msgTs;
|
|
731
|
+
turnIndex++;
|
|
732
|
+
thinkingIdx = 0; // reset thinking index for the new turn
|
|
694
733
|
// Extract optional images from user message content array
|
|
695
734
|
let images = [];
|
|
696
735
|
if (Array.isArray(m.content)) {
|
|
@@ -705,17 +744,19 @@ function reconstructFromEntries(entries, timing = null) {
|
|
|
705
744
|
out.push({ role: "user", text: extractContentText(m.content), images, ts: msgTs });
|
|
706
745
|
} else if (m.role === "assistant") {
|
|
707
746
|
let turnDurationMs = null;
|
|
708
|
-
// Try timing data first, then fall back to timestamp heuristic
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
if (
|
|
715
|
-
|
|
747
|
+
// Try timing data first, then fall back to timestamp heuristic.
|
|
748
|
+
// Only show turn duration on the last assistant message of the turn.
|
|
749
|
+
const turnTiming = timing ? timing[turnIndex] : null;
|
|
750
|
+
if (lastAssistantInTurn.has(i)) {
|
|
751
|
+
if (turnTiming?.turnDuration != null) {
|
|
752
|
+
turnDurationMs = turnTiming.turnDuration;
|
|
753
|
+
} else if (lastUserTs && msgTs && msgTs >= lastUserTs) {
|
|
754
|
+
const diff = msgTs - lastUserTs;
|
|
755
|
+
if (diff > 0 && diff < 15 * 60 * 1000) {
|
|
756
|
+
turnDurationMs = diff;
|
|
757
|
+
}
|
|
716
758
|
}
|
|
717
759
|
}
|
|
718
|
-
let thinkingIdx = 0;
|
|
719
760
|
const rawContent = Array.isArray(m.content) ? m.content : (m.content ? [{ type: "text", text: String(m.content) }] : []);
|
|
720
761
|
const content = rawContent.map(part => {
|
|
721
762
|
if (part && part.type === "thinking") {
|
|
@@ -753,13 +794,11 @@ function reconstructFromEntries(entries, timing = null) {
|
|
|
753
794
|
content.push({ type: "text", text: `⚠️ **生成失败**: ${errMsg}` });
|
|
754
795
|
}
|
|
755
796
|
out.push({ role: "assistant", content, ts: msgTs, turnDurationMs, usage: m.usage });
|
|
756
|
-
assistantMsgCount++;
|
|
757
797
|
}
|
|
758
798
|
// toolResult entries are attached directly to assistant toolCall parts, so they don't produce standalone messages
|
|
759
799
|
}
|
|
760
800
|
return out;
|
|
761
801
|
}
|
|
762
|
-
|
|
763
802
|
function extractContentText(content) {
|
|
764
803
|
if (typeof content === "string") return content;
|
|
765
804
|
if (!Array.isArray(content)) return "";
|
|
@@ -1025,6 +1064,22 @@ async function handleIncomingFiles(files) {
|
|
|
1025
1064
|
|
|
1026
1065
|
const handleImageFiles = handleIncomingFiles;
|
|
1027
1066
|
|
|
1067
|
+
/**
|
|
1068
|
+
* 手动压缩当前会话上下文,释放长会话累积的 token,缓解越聊越慢。
|
|
1069
|
+
* 触发后由 pi 端执行 compaction,返回结果通过 response 事件回显。
|
|
1070
|
+
*/
|
|
1071
|
+
function compactContext() {
|
|
1072
|
+
if (state.streaming) {
|
|
1073
|
+
showToast("请等待当前任务结束后再压缩上下文");
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
if (!sendWs({ type: "compact" })) {
|
|
1077
|
+
showToast("连接不可用,无法压缩上下文");
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
showToast("正在压缩上下文…");
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1028
1083
|
function exportCurrentSession() {
|
|
1029
1084
|
const chatInner = $("#chat-inner");
|
|
1030
1085
|
if (!chatInner || chatInner.children.length === 0) {
|
|
@@ -1388,10 +1443,25 @@ function summaryArgs(name, args) {
|
|
|
1388
1443
|
} catch { return ""; }
|
|
1389
1444
|
}
|
|
1390
1445
|
|
|
1391
|
-
|
|
1446
|
+
let userScrolledUp = false;
|
|
1447
|
+
|
|
1448
|
+
function updatePageTitle(title) {
|
|
1449
|
+
if (!title || title === "新对话") {
|
|
1450
|
+
document.title = "pi-chat";
|
|
1451
|
+
} else {
|
|
1452
|
+
document.title = `${title} · pi-chat`;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function scrollBottom(force = false) {
|
|
1392
1457
|
// Don't fight the user during a background-event replay (backfill).
|
|
1393
1458
|
if (state.isBackfilling) return;
|
|
1394
1459
|
const chat = $("#chat");
|
|
1460
|
+
if (!chat) return;
|
|
1461
|
+
// If streaming and user manually scrolled up to read history, don't hijack scroll unless forced!
|
|
1462
|
+
if (!force && userScrolledUp && state.streaming) {
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1395
1465
|
chat.scrollTop = chat.scrollHeight;
|
|
1396
1466
|
}
|
|
1397
1467
|
|
|
@@ -1873,6 +1943,19 @@ function handlePiMessage(obj) {
|
|
|
1873
1943
|
state.aborting = false;
|
|
1874
1944
|
setComposerStreaming(false);
|
|
1875
1945
|
}
|
|
1946
|
+
else if (obj.command === "compact") {
|
|
1947
|
+
if (obj.success) {
|
|
1948
|
+
const after = obj.data?.estimatedTokensAfter;
|
|
1949
|
+
const msg = typeof after === "number"
|
|
1950
|
+
? `上下文已压缩(预估剩余约 ${after} tokens)`
|
|
1951
|
+
: "上下文已压缩";
|
|
1952
|
+
showToast(msg);
|
|
1953
|
+
appendSystemNotice(msg);
|
|
1954
|
+
sendWs({ type: "get_state" });
|
|
1955
|
+
} else {
|
|
1956
|
+
showToast(`压缩上下文失败: ${obj.error || "未知错误"}`);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1876
1959
|
else if (obj.command === "switch_session" && obj.success) {
|
|
1877
1960
|
// ask pi for current state so we can get session id, name
|
|
1878
1961
|
sendWs({ type: "get_state" });
|
|
@@ -2838,6 +2921,8 @@ function submitSteer() {
|
|
|
2838
2921
|
appendMessageNode("user", { text, isSteer: true, ts: Date.now() });
|
|
2839
2922
|
ta.value = "";
|
|
2840
2923
|
autoResize();
|
|
2924
|
+
userScrolledUp = false;
|
|
2925
|
+
scrollBottom(true);
|
|
2841
2926
|
updateComposerUI();
|
|
2842
2927
|
|
|
2843
2928
|
if (hint) hint.textContent = "已插入指导指令!pi 将在当前轮次中实时接收并调整方向。";
|
|
@@ -2882,6 +2967,8 @@ function submitPrompt() {
|
|
|
2882
2967
|
const now = Date.now();
|
|
2883
2968
|
// Render the user's message locally for instant feedback.
|
|
2884
2969
|
appendMessageNode("user", { text, images: state.attachedImages ? [...state.attachedImages] : [], ts: now });
|
|
2970
|
+
userScrolledUp = false;
|
|
2971
|
+
scrollBottom(true);
|
|
2885
2972
|
|
|
2886
2973
|
ta.value = "";
|
|
2887
2974
|
state.attachedImages = [];
|
|
@@ -2898,6 +2985,7 @@ function submitPrompt() {
|
|
|
2898
2985
|
if (state.currentSessionFile == null && text) {
|
|
2899
2986
|
const promptTitle = text.slice(0, 60).replace(/\s+/g, " ");
|
|
2900
2987
|
$("#topSessionName").textContent = promptTitle;
|
|
2988
|
+
updatePageTitle(promptTitle);
|
|
2901
2989
|
sendWs({ type: "set_session_name", name: promptTitle });
|
|
2902
2990
|
|
|
2903
2991
|
// Update draft session item in sidebar immediately
|
|
@@ -2929,8 +3017,18 @@ function submitPrompt() {
|
|
|
2929
3017
|
state.streamingMsgDurationEl = null;
|
|
2930
3018
|
if (state.streamingMsg) { state.streamingMsg.remove(); state.streamingMsg = null; }
|
|
2931
3019
|
state.streamingItems = [];
|
|
3020
|
+
// Restore user input and attachments so text is not lost
|
|
3021
|
+
if (ta) {
|
|
3022
|
+
ta.value = text;
|
|
3023
|
+
autoResize();
|
|
3024
|
+
ta.focus();
|
|
3025
|
+
}
|
|
3026
|
+
if (imagesToSend && imagesToSend.length > 0) {
|
|
3027
|
+
state.attachedImages = imagesToSend;
|
|
3028
|
+
renderImagePreviews();
|
|
3029
|
+
}
|
|
2932
3030
|
const hint = $(".composer-hint");
|
|
2933
|
-
if (hint) hint.textContent = "发送失败:WebSocket
|
|
3031
|
+
if (hint) hint.textContent = "发送失败:WebSocket 连接已断开,已为你恢复输入内容。正在尝试重连…";
|
|
2934
3032
|
scheduleReconnect(0);
|
|
2935
3033
|
}
|
|
2936
3034
|
}
|
|
@@ -3142,6 +3240,12 @@ async function init() {
|
|
|
3142
3240
|
exportBtn.addEventListener("click", exportCurrentSession);
|
|
3143
3241
|
}
|
|
3144
3242
|
|
|
3243
|
+
// Compact context button (manual compaction for long/slow sessions)
|
|
3244
|
+
const compactBtn = $("#btnCompactChat");
|
|
3245
|
+
if (compactBtn) {
|
|
3246
|
+
compactBtn.addEventListener("click", compactContext);
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3145
3249
|
// Image and file attach / picker / paste / drag-and-drop
|
|
3146
3250
|
const btnAttach = $("#btnAttachImage");
|
|
3147
3251
|
const fileInput = $("#imageFileInput");
|
package/public/index.html
CHANGED
|
@@ -76,6 +76,14 @@
|
|
|
76
76
|
</svg>
|
|
77
77
|
<span class="topbar-action-label">导出</span>
|
|
78
78
|
</button>
|
|
79
|
+
<button class="btn-topbar-action" id="btnCompactChat" title="压缩当前上下文(长会话变慢时使用)">
|
|
80
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
81
|
+
<polyline points="4 7 4 4 20 4 20 7"></polyline>
|
|
82
|
+
<line x1="9" y1="20" x2="15" y2="20"></line>
|
|
83
|
+
<line x1="12" y1="4" x2="12" y2="20"></line>
|
|
84
|
+
</svg>
|
|
85
|
+
<span class="topbar-action-label">压缩</span>
|
|
86
|
+
</button>
|
|
79
87
|
<div class="model-menu" id="modelMenu"></div>
|
|
80
88
|
<div class="thinking-menu" id="thinkingMenu"></div>
|
|
81
89
|
</div>
|
package/public/markdown.js
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
function sanitizeUrl(url) {
|
|
43
43
|
if (!url) return "#";
|
|
44
44
|
const trimmed = url.trim();
|
|
45
|
-
if (/^(https?:\/\/|mailto
|
|
45
|
+
if (/^(https?:\/\/|mailto:|#)/i.test(trimmed)) {
|
|
46
46
|
return trimmed.replace(/"/g, """).replace(/'/g, "'");
|
|
47
47
|
}
|
|
48
48
|
return "#";
|
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
}
|
|
112
112
|
let outHtml = "";
|
|
113
113
|
let para = [];
|
|
114
|
+
let quoteLines = [];
|
|
114
115
|
let linkListOpen = null;
|
|
115
116
|
let linkListOrdered = null;
|
|
116
117
|
|
|
@@ -122,6 +123,13 @@
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
function flushQuote() {
|
|
127
|
+
if (quoteLines.length === 0) return;
|
|
128
|
+
const block = quoteLines.map(l => mdInlineBlock(l)).join("<br>");
|
|
129
|
+
quoteLines = [];
|
|
130
|
+
outHtml += `<blockquote>${block}</blockquote>`;
|
|
131
|
+
}
|
|
132
|
+
|
|
125
133
|
function flushPara() {
|
|
126
134
|
if (para.length === 0) return;
|
|
127
135
|
const block = para.join("\n").trim();
|
|
@@ -132,30 +140,35 @@
|
|
|
132
140
|
if (seg.kind === "table") {
|
|
133
141
|
flushPara();
|
|
134
142
|
flushList();
|
|
143
|
+
flushQuote();
|
|
135
144
|
outHtml += mdTable(seg.lines);
|
|
136
145
|
} else if (seg.kind === "line") {
|
|
137
146
|
// horizontal rule
|
|
138
147
|
if (/^\s*([-*_])\s*\1\s*\1[\s\-_*]*$/.test(seg.text)) {
|
|
139
148
|
flushPara();
|
|
140
149
|
flushList();
|
|
150
|
+
flushQuote();
|
|
141
151
|
outHtml += "<hr>";
|
|
142
152
|
} else if (/^(#{1,6})\s+(.*)$/.test(seg.text)) {
|
|
143
153
|
// headings
|
|
144
154
|
const m = seg.text.match(/^(#{1,6})\s+(.*)$/);
|
|
145
155
|
flushPara();
|
|
146
156
|
flushList();
|
|
157
|
+
flushQuote();
|
|
147
158
|
const level = m[1].length;
|
|
148
159
|
outHtml += `<h${level}>${mdInlineBlock(m[2])}</h${level}>`;
|
|
149
160
|
} else if (/^\s*$/.test(seg.text)) {
|
|
150
161
|
flushPara();
|
|
151
162
|
flushList();
|
|
163
|
+
flushQuote();
|
|
152
164
|
} else if (/^>\s?/.test(seg.text)) {
|
|
153
|
-
// blockquote line
|
|
165
|
+
// blockquote line (accumulate adjacent lines into a single blockquote)
|
|
154
166
|
flushPara();
|
|
155
167
|
flushList();
|
|
156
|
-
|
|
168
|
+
quoteLines.push(seg.text.replace(/^>\s?/, ""));
|
|
157
169
|
} else if (/^\s*[-*+]\s+/.test(seg.text) || /^\s*\d+\.\s+/.test(seg.text)) {
|
|
158
170
|
// list item
|
|
171
|
+
flushQuote();
|
|
159
172
|
const isOrdered = /^\s*\d+\.\s+/.test(seg.text);
|
|
160
173
|
if (!linkListOpen || linkListOrdered !== isOrdered) {
|
|
161
174
|
flushPara();
|
|
@@ -176,11 +189,13 @@
|
|
|
176
189
|
outHtml += `<li>${taskPrefix}${mdInlineBlock(itemText)}</li>`;
|
|
177
190
|
} else {
|
|
178
191
|
flushList();
|
|
192
|
+
flushQuote();
|
|
179
193
|
para.push(seg.text);
|
|
180
194
|
}
|
|
181
195
|
}
|
|
182
196
|
}
|
|
183
197
|
flushList();
|
|
198
|
+
flushQuote();
|
|
184
199
|
flushPara();
|
|
185
200
|
// restore inline code
|
|
186
201
|
outHtml = outHtml.replace(/\u0000CODE(\d+)\u0000/g, (_, n) => {
|
package/public/style.css
CHANGED
|
@@ -741,6 +741,9 @@ body {
|
|
|
741
741
|
stroke-linejoin: round;
|
|
742
742
|
flex-shrink: 0;
|
|
743
743
|
}
|
|
744
|
+
.btn-copy-code svg *, .btn-copy-msg svg *, .btn-copy-tool svg * {
|
|
745
|
+
fill: none;
|
|
746
|
+
}
|
|
744
747
|
.msg.assistant .content ul, .msg.assistant .content ol { margin: 8px 0 12px 24px; }
|
|
745
748
|
.msg.assistant .content li { margin: 4px 0; }
|
|
746
749
|
.msg.assistant .content h1, .msg.assistant .content h2, .msg.assistant .content h3,
|