@liguoshuai/pi-web-chat 2.17.8 → 2.19.1

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@liguoshuai/pi-web-chat.svg)](https://www.npmjs.com/package/@liguoshuai/pi-web-chat)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
- [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-brightgreen.svg)](https://nodejs.org/)
5
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-brightgreen.svg)](https://nodejs.org/)
6
6
 
7
7
  一个 [pi](https://pi.dev) 编程代理的 Web 界面,风格参考 ChatGPT / Gemini ——
8
8
  左侧历史会话侧边栏 + 右侧对话区 + 底部输入框。底层通过 pi 的 **RPC 模式**
@@ -35,7 +35,7 @@
35
35
  ## 🚀 快速开始
36
36
 
37
37
  ### 1. 环境准备
38
- - **Node.js**: `>= 18.0.0`
38
+ - **Node.js**: `>= 20.0.0`
39
39
  - **操作系统**: Linux / macOS / WSL (Windows)
40
40
 
41
41
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liguoshuai/pi-web-chat",
3
- "version": "2.17.8",
3
+ "version": "2.19.1",
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",
@@ -37,13 +37,13 @@
37
37
  "access": "public"
38
38
  },
39
39
  "engines": {
40
- "node": ">=18"
40
+ "node": ">=20"
41
41
  },
42
42
  "dependencies": {
43
43
  "express": "^4.21.2",
44
44
  "ws": "^8.18.0",
45
- "@liguoshuai/pi-chat-protocol": "2.17.8",
46
- "@liguoshuai/pi-chat-server": "2.17.8"
45
+ "@liguoshuai/pi-chat-protocol": "2.19.1",
46
+ "@liguoshuai/pi-chat-server": "2.19.1"
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
@@ -168,8 +168,17 @@ async function loadServerConfig() {
168
168
  }
169
169
  if (data.version) {
170
170
  state.version = data.version;
171
- const verEl = $("#appVersion");
172
- if (verEl) verEl.textContent = `v${data.version}`;
171
+ }
172
+ if (data.piVersion) {
173
+ state.piVersion = data.piVersion;
174
+ }
175
+ const piVerEl = $("#piVersion");
176
+ if (piVerEl) {
177
+ piVerEl.textContent = state.piVersion ? ` v${state.piVersion}` : "";
178
+ }
179
+ const verEl = $("#appVersion");
180
+ if (verEl) {
181
+ verEl.textContent = state.version ? ` v${state.version}` : "";
173
182
  }
174
183
  if (data.defaultModel) {
175
184
  state.defaultModel = data.defaultModel;
@@ -372,13 +381,7 @@ function startStreamingTimer() {
372
381
  el.style.display = "";
373
382
  }
374
383
  });
375
- const liveTools = document.querySelectorAll(".tool-duration.live");
376
- liveTools.forEach((el) => {
377
- if (el._startedAt) {
378
- el.textContent = formatDuration(now - el._startedAt);
379
- el.style.display = "";
380
- }
381
- });
384
+
382
385
  // Live update for the thinking placeholder spinner label before first delta arrives
383
386
  const placeholder = document.querySelector(".thinking-placeholder");
384
387
  if (placeholder && state.turnStartedAt) {
@@ -393,7 +396,7 @@ function startStreamingTimer() {
393
396
  }
394
397
  }
395
398
  }
396
- }, 100);
399
+ }, 200);
397
400
  }
398
401
 
399
402
  function stopStreamingTimer() {
@@ -763,6 +766,7 @@ function reconstructFromEntries(entries, timing = null) {
763
766
  lastUserTs = msgTs;
764
767
  turnIndex++;
765
768
  thinkingIdx = 0; // reset thinking index for the new turn
769
+ const isSteer = Boolean(m.isSteer || e.isSteer || e.customType === "steer" || m.customType === "steer");
766
770
  // Extract optional images from user message content array
767
771
  let images = [];
768
772
  if (Array.isArray(m.content)) {
@@ -774,7 +778,7 @@ function reconstructFromEntries(entries, timing = null) {
774
778
  url: c.data ? `data:${c.mimeType || "image/png"};base64,${c.data}` : (c.url || "")
775
779
  }));
776
780
  }
777
- out.push({ role: "user", text: extractContentText(m.content), images, ts: msgTs });
781
+ out.push({ role: "user", text: extractContentText(m.content), images, ts: msgTs, isSteer });
778
782
  } else if (m.role === "assistant") {
779
783
  let turnDurationMs = null;
780
784
  // Try timing data first, then fall back to timestamp heuristic.
@@ -842,10 +846,19 @@ function extractContentText(content) {
842
846
  .join("\n\n");
843
847
  }
844
848
 
845
- function appendSystemNotice(text) {
849
+ function appendSystemNotice(text, replaceIfLast = true) {
846
850
  if (!text) return;
847
851
  const chatInner = $("#chat-inner");
848
852
  if (!chatInner) return;
853
+ const lastChild = chatInner.lastElementChild;
854
+ if (replaceIfLast && lastChild && lastChild.classList.contains("system-notice-divider")) {
855
+ const textEl = lastChild.querySelector(".system-notice-text");
856
+ if (textEl) {
857
+ textEl.textContent = text;
858
+ scrollBottom();
859
+ return;
860
+ }
861
+ }
849
862
  const node = el("div", { class: "system-notice-divider" }, [
850
863
  el("span", { class: "system-notice-text", text })
851
864
  ]);
@@ -1657,6 +1670,9 @@ function refreshStreamingContent() {
1657
1670
  el("span", { class: "thinking-spinner" }),
1658
1671
  el("span", { class: "thinking-label", text: "正在思考中…" })
1659
1672
  ]));
1673
+ content.appendChild(el("div", { class: "streaming-cursor-row" }, [
1674
+ el("span", { class: "typing-cursor" })
1675
+ ]));
1660
1676
  }
1661
1677
  }
1662
1678
  scrollBottom();
@@ -1669,7 +1685,7 @@ function refreshStreamingContent() {
1669
1685
  const lastItem = items[items.length - 1];
1670
1686
  if (lastItem.type === "text") {
1671
1687
  const lastEl = content.lastElementChild;
1672
- if (lastEl && !lastEl.classList.contains("thinking-block") && !lastEl.classList.contains("tool-block")) {
1688
+ if (lastEl && !lastEl.classList.contains("thinking-block") && !lastEl.classList.contains("tool-block") && !lastEl.classList.contains("streaming-cursor-row")) {
1673
1689
  const showCursor = state.streaming;
1674
1690
  lastEl.innerHTML = renderMarkdown(lastItem.text) + (showCursor ? '<span class="typing-cursor"></span>' : "");
1675
1691
  scrollBottom();
@@ -1681,6 +1697,7 @@ function refreshStreamingContent() {
1681
1697
  // Full rebuild (new items added or structure changed)
1682
1698
  content.innerHTML = "";
1683
1699
  const lastIdx = items.length - 1;
1700
+ let hasText = false;
1684
1701
  for (let i = 0; i < items.length; i++) {
1685
1702
  const item = items[i];
1686
1703
  const isLast = (i === lastIdx);
@@ -1690,10 +1707,16 @@ function refreshStreamingContent() {
1690
1707
  } else if (item.type === "tool") {
1691
1708
  if (item.tc?.block) content.appendChild(item.tc.block);
1692
1709
  } else if (item.type === "text") {
1710
+ hasText = true;
1693
1711
  const showCursor = state.streaming && isLast;
1694
1712
  content.appendChild(el("div", { html: renderMarkdown(item.text) + (showCursor ? '<span class="typing-cursor"></span>' : "") }));
1695
1713
  }
1696
1714
  }
1715
+ if (state.streaming && !hasText) {
1716
+ content.appendChild(el("div", { class: "streaming-cursor-row" }, [
1717
+ el("span", { class: "typing-cursor" })
1718
+ ]));
1719
+ }
1697
1720
  lastRenderedItemCount = items.length;
1698
1721
  scrollBottom();
1699
1722
  }
@@ -3077,6 +3100,13 @@ function submitSteer() {
3077
3100
  return;
3078
3101
  }
3079
3102
 
3103
+ // If there was an active streaming assistant block before steering, finalize it
3104
+ // so that subsequent assistant chunks render in a new block below this steer message!
3105
+ if (state.streamingMsg) {
3106
+ finalizeStreamingMsg();
3107
+ state.streaming = true; // resume streaming flag for subsequent response
3108
+ }
3109
+
3080
3110
  appendMessageNode("user", { text, isSteer: true, ts: Date.now() });
3081
3111
  ta.value = "";
3082
3112
  autoResize();
@@ -3144,7 +3174,7 @@ function submitPrompt() {
3144
3174
  if (state.streamingWatchdog) clearTimeout(state.streamingWatchdog);
3145
3175
  state.streamingWatchdog = setTimeout(() => {
3146
3176
  if (state.streaming) {
3147
- toast("生成超时(5 分钟无响应),已自动结束。", "warn");
3177
+ showToast("生成超时(5 分钟无响应),已自动结束。");
3148
3178
  finalizeStreamingMsg();
3149
3179
  }
3150
3180
  }, 5 * 60 * 1000);
package/public/index.html CHANGED
@@ -30,11 +30,10 @@
30
30
  <div id="connStatus" class="conn-status" title="点击可重新连接"><span id="connDot" style="color: var(--danger);">●</span>&nbsp;<span id="connLabel">连接中…</span></div>
31
31
  <div class="sidebar-bottom-meta">
32
32
  <div class="sidebar-bottom-links">
33
- <a href="https://pi.dev" target="_blank" rel="noopener">pi.dev</a>
33
+ <a href="https://pi.dev" target="_blank" rel="noopener">pi<span id="piVersion"></span></a>
34
34
  <span class="sidebar-link-sep">·</span>
35
- <a href="https://github.com/liguoshuai-1990/pi-web-chat" target="_blank" rel="noopener">pi-web-chat</a>
35
+ <a href="https://github.com/liguoshuai-1990/pi-chat" target="_blank" rel="noopener">pi-chat<span id="appVersion"></span></a>
36
36
  </div>
37
- <span id="appVersion" class="app-version" title="pi-web-chat 版本"></span>
38
37
  </div>
39
38
  </div>
40
39
  <div class="sidebar-resizer" id="sidebarResizer" title="拖拽调整侧边栏宽度,双击恢复默认"></div>
package/public/style.css CHANGED
@@ -342,7 +342,6 @@ body {
342
342
  margin-top: 6px;
343
343
  display: flex;
344
344
  align-items: center;
345
- justify-content: space-between;
346
345
  gap: 8px;
347
346
  }
348
347
  .sidebar-bottom-links {
@@ -928,6 +927,13 @@ body {
928
927
  to { opacity: 1; transform: scale(1.25); }
929
928
  }
930
929
 
930
+ .streaming-cursor-row {
931
+ margin-top: 6px;
932
+ line-height: 1;
933
+ }
934
+ .streaming-cursor-row .typing-cursor::after {
935
+ margin-left: 0;
936
+ }
931
937
  .typing-cursor::after {
932
938
  content: "▋"; display: inline-block; margin-left: 2px;
933
939
  animation: blink 1s steps(2) infinite; color: var(--accent);