@liguoshuai/pi-web-chat 2.18.13 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liguoshuai/pi-web-chat",
3
- "version": "2.18.13",
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",
@@ -42,8 +42,8 @@
42
42
  "dependencies": {
43
43
  "express": "^4.21.2",
44
44
  "ws": "^8.18.0",
45
- "@liguoshuai/pi-chat-server": "2.18.13",
46
- "@liguoshuai/pi-chat-protocol": "2.18.13"
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;
@@ -757,6 +766,7 @@ function reconstructFromEntries(entries, timing = null) {
757
766
  lastUserTs = msgTs;
758
767
  turnIndex++;
759
768
  thinkingIdx = 0; // reset thinking index for the new turn
769
+ const isSteer = Boolean(m.isSteer || e.isSteer || e.customType === "steer" || m.customType === "steer");
760
770
  // Extract optional images from user message content array
761
771
  let images = [];
762
772
  if (Array.isArray(m.content)) {
@@ -768,7 +778,7 @@ function reconstructFromEntries(entries, timing = null) {
768
778
  url: c.data ? `data:${c.mimeType || "image/png"};base64,${c.data}` : (c.url || "")
769
779
  }));
770
780
  }
771
- out.push({ role: "user", text: extractContentText(m.content), images, ts: msgTs });
781
+ out.push({ role: "user", text: extractContentText(m.content), images, ts: msgTs, isSteer });
772
782
  } else if (m.role === "assistant") {
773
783
  let turnDurationMs = null;
774
784
  // Try timing data first, then fall back to timestamp heuristic.
@@ -836,10 +846,19 @@ function extractContentText(content) {
836
846
  .join("\n\n");
837
847
  }
838
848
 
839
- function appendSystemNotice(text) {
849
+ function appendSystemNotice(text, replaceIfLast = true) {
840
850
  if (!text) return;
841
851
  const chatInner = $("#chat-inner");
842
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
+ }
843
862
  const node = el("div", { class: "system-notice-divider" }, [
844
863
  el("span", { class: "system-notice-text", text })
845
864
  ]);
@@ -1651,6 +1670,9 @@ function refreshStreamingContent() {
1651
1670
  el("span", { class: "thinking-spinner" }),
1652
1671
  el("span", { class: "thinking-label", text: "正在思考中…" })
1653
1672
  ]));
1673
+ content.appendChild(el("div", { class: "streaming-cursor-row" }, [
1674
+ el("span", { class: "typing-cursor" })
1675
+ ]));
1654
1676
  }
1655
1677
  }
1656
1678
  scrollBottom();
@@ -1663,7 +1685,7 @@ function refreshStreamingContent() {
1663
1685
  const lastItem = items[items.length - 1];
1664
1686
  if (lastItem.type === "text") {
1665
1687
  const lastEl = content.lastElementChild;
1666
- 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")) {
1667
1689
  const showCursor = state.streaming;
1668
1690
  lastEl.innerHTML = renderMarkdown(lastItem.text) + (showCursor ? '<span class="typing-cursor"></span>' : "");
1669
1691
  scrollBottom();
@@ -1675,6 +1697,7 @@ function refreshStreamingContent() {
1675
1697
  // Full rebuild (new items added or structure changed)
1676
1698
  content.innerHTML = "";
1677
1699
  const lastIdx = items.length - 1;
1700
+ let hasText = false;
1678
1701
  for (let i = 0; i < items.length; i++) {
1679
1702
  const item = items[i];
1680
1703
  const isLast = (i === lastIdx);
@@ -1684,10 +1707,16 @@ function refreshStreamingContent() {
1684
1707
  } else if (item.type === "tool") {
1685
1708
  if (item.tc?.block) content.appendChild(item.tc.block);
1686
1709
  } else if (item.type === "text") {
1710
+ hasText = true;
1687
1711
  const showCursor = state.streaming && isLast;
1688
1712
  content.appendChild(el("div", { html: renderMarkdown(item.text) + (showCursor ? '<span class="typing-cursor"></span>' : "") }));
1689
1713
  }
1690
1714
  }
1715
+ if (state.streaming && !hasText) {
1716
+ content.appendChild(el("div", { class: "streaming-cursor-row" }, [
1717
+ el("span", { class: "typing-cursor" })
1718
+ ]));
1719
+ }
1691
1720
  lastRenderedItemCount = items.length;
1692
1721
  scrollBottom();
1693
1722
  }
@@ -3071,6 +3100,13 @@ function submitSteer() {
3071
3100
  return;
3072
3101
  }
3073
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
+
3074
3110
  appendMessageNode("user", { text, isSteer: true, ts: Date.now() });
3075
3111
  ta.value = "";
3076
3112
  autoResize();
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);