@privateclaw/privateclaw-relay 0.1.5 → 0.1.7

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.
@@ -1,4 +1,4 @@
1
- import { applyTranslations, bindLocaleSelect, onLocaleChange, t } from "./i18n.js?v=20260316-3";
1
+ import { applyTranslations, bindLocaleSelect, onLocaleChange, t } from "./i18n.js?v=20260318-1";
2
2
  import {
3
3
  createIdentity,
4
4
  decodeBase64,
@@ -7,7 +7,7 @@ import {
7
7
  inviteUsesNonDefaultRelay,
8
8
  readFileAsAttachment,
9
9
  } from "./protocol-web.js?v=20260316-1";
10
- import { PrivateClawWebSessionClient } from "./session-client.js?v=20260316-1";
10
+ import { PrivateClawWebSessionClient } from "./session-client.js?v=20260318-1";
11
11
 
12
12
  const MAX_INLINE_ATTACHMENT_BYTES = 5 * 1024 * 1024;
13
13
  const QR_SCAN_MAX_DIMENSION = 1440;
@@ -60,6 +60,7 @@ const state = {
60
60
  client: null,
61
61
  invite: null,
62
62
  messages: [],
63
+ expandedThinkingTraceIds: new Set(),
63
64
  commands: [],
64
65
  participants: [],
65
66
  selectedAttachments: [],
@@ -582,6 +583,7 @@ function clearObjectUrls() {
582
583
  function resetConversationState() {
583
584
  clearObjectUrls();
584
585
  state.messages = [];
586
+ state.expandedThinkingTraceIds.clear();
585
587
  state.commands = [];
586
588
  state.participants = [];
587
589
  state.selectedAttachments = [];
@@ -749,6 +751,145 @@ function renderAttachments(attachments) {
749
751
  return container;
750
752
  }
751
753
 
754
+ function isThinkingTraceMessage(message) {
755
+ return typeof message?.thinkingStatus === "string";
756
+ }
757
+
758
+ function isThinkingTraceActive(message) {
759
+ return message?.thinkingStatus === "started" || message?.thinkingStatus === "streaming";
760
+ }
761
+
762
+ function createThinkingTraceBadge({ label, tone = "neutral" }) {
763
+ const badge = document.createElement("span");
764
+ badge.className = `thinking-trace-badge ${tone}`;
765
+ badge.textContent = label;
766
+ return badge;
767
+ }
768
+
769
+ function getThinkingTraceIcon(kind, { active = false, failed = false } = {}) {
770
+ if (failed || kind === "error") {
771
+ return "!";
772
+ }
773
+ if (active || kind === "thought") {
774
+ return "◎";
775
+ }
776
+ if (kind === "result") {
777
+ return "✓";
778
+ }
779
+ return "⚙";
780
+ }
781
+
782
+ function renderThinkingTraceEntry(entry) {
783
+ const card = document.createElement("div");
784
+ card.className = `thinking-trace-entry ${entry.kind || "thought"}`;
785
+
786
+ const header = document.createElement("div");
787
+ header.className = "thinking-trace-entry-header";
788
+
789
+ const icon = document.createElement("span");
790
+ icon.className = `thinking-trace-icon ${entry.kind || "thought"}`;
791
+ icon.textContent = getThinkingTraceIcon(entry.kind);
792
+
793
+ const title = document.createElement("strong");
794
+ title.textContent = entry.title || t("chat.thinkingTraceFallbackTitle");
795
+
796
+ header.append(icon, title);
797
+ if (entry.toolName) {
798
+ header.append(createThinkingTraceBadge({ label: entry.toolName, tone: "tool" }));
799
+ }
800
+
801
+ card.append(header);
802
+ if (typeof entry.text === "string" && entry.text.trim() !== "") {
803
+ const body = document.createElement("div");
804
+ body.className = "thinking-trace-entry-body";
805
+ body.append(renderRichText(entry.text));
806
+ card.append(body);
807
+ }
808
+ return card;
809
+ }
810
+
811
+ function renderThinkingTrace(message) {
812
+ const entries = Array.isArray(message.thinkingEntries) ? message.thinkingEntries : [];
813
+ const latestEntry = entries[entries.length - 1] || null;
814
+ const active = isThinkingTraceActive(message);
815
+ const failed = message.thinkingStatus === "failed";
816
+ const title = latestEntry?.title || t("chat.thinkingTraceFallbackTitle");
817
+ const previewSource =
818
+ (typeof message.thinkingSummary === "string" && message.thinkingSummary.trim()) ||
819
+ (typeof latestEntry?.text === "string" && latestEntry.text.trim()) ||
820
+ t("chat.pendingLabel");
821
+
822
+ const card = document.createElement(entries.length > 0 ? "details" : "div");
823
+ card.className = `thinking-trace-card${active ? " active" : ""}${failed ? " failed" : ""}`;
824
+ if (card instanceof HTMLDetailsElement) {
825
+ card.open = active && state.expandedThinkingTraceIds.has(message.id);
826
+ card.addEventListener("toggle", () => {
827
+ if (card.open && active) {
828
+ state.expandedThinkingTraceIds.add(message.id);
829
+ } else {
830
+ state.expandedThinkingTraceIds.delete(message.id);
831
+ }
832
+ });
833
+ }
834
+
835
+ const summary = document.createElement(entries.length > 0 ? "summary" : "div");
836
+ summary.className = "thinking-trace-summary";
837
+
838
+ const header = document.createElement("div");
839
+ header.className = "thinking-trace-header";
840
+
841
+ const headerMain = document.createElement("div");
842
+ headerMain.className = "thinking-trace-header-main";
843
+
844
+ const icon = document.createElement("span");
845
+ icon.className = `thinking-trace-icon ${failed ? "error" : latestEntry?.kind || "thought"}${active ? " active" : ""}`;
846
+ icon.textContent = getThinkingTraceIcon(latestEntry?.kind, { active, failed });
847
+
848
+ const heading = document.createElement("strong");
849
+ heading.textContent = title;
850
+
851
+ headerMain.append(icon, heading);
852
+ header.append(headerMain);
853
+
854
+ const badges = document.createElement("div");
855
+ badges.className = "thinking-trace-badges";
856
+ badges.append(
857
+ createThinkingTraceBadge({
858
+ label: active
859
+ ? t("chat.thinkingTraceLive")
860
+ : failed
861
+ ? t("chat.thinkingTraceFailed")
862
+ : t("chat.thinkingTraceDone"),
863
+ tone: failed ? "error" : active ? "live" : "done",
864
+ }),
865
+ );
866
+ if (latestEntry?.toolName) {
867
+ badges.append(createThinkingTraceBadge({ label: latestEntry.toolName, tone: "tool" }));
868
+ }
869
+ badges.append(createThinkingTraceBadge({
870
+ label: t("chat.thinkingTraceSteps", { count: String(entries.length) }),
871
+ }));
872
+ header.append(badges);
873
+
874
+ const preview = document.createElement("p");
875
+ preview.className = "thinking-trace-preview";
876
+ preview.textContent = previewSource;
877
+
878
+ summary.append(header, preview);
879
+ card.append(summary);
880
+
881
+ if (entries.length > 0) {
882
+ const history = document.createElement("div");
883
+ history.className = "thinking-trace-history";
884
+ for (const entry of entries) {
885
+ history.append(renderThinkingTraceEntry(entry));
886
+ }
887
+ card.append(history);
888
+ }
889
+
890
+ return card;
891
+ }
892
+
752
893
  function upsertMessage(message) {
753
894
  if (message.isPending && message.replyTo) {
754
895
  const repliedIndex = state.messages.findIndex(
@@ -768,6 +909,14 @@ function upsertMessage(message) {
768
909
 
769
910
  const existingIndex = state.messages.findIndex((item) => item.id === message.id);
770
911
  if (existingIndex >= 0) {
912
+ if (isThinkingTraceMessage(message) && !isThinkingTraceActive(message) && !(message.thinkingEntries?.length > 0)) {
913
+ state.messages.splice(existingIndex, 1);
914
+ state.expandedThinkingTraceIds.delete(message.id);
915
+ return;
916
+ }
917
+ if (isThinkingTraceMessage(message) && !isThinkingTraceActive(message)) {
918
+ state.expandedThinkingTraceIds.delete(message.id);
919
+ }
771
920
  const existingMessage = state.messages[existingIndex];
772
921
  state.messages[existingIndex] = {
773
922
  ...message,
@@ -781,12 +930,17 @@ function upsertMessage(message) {
781
930
  return;
782
931
  }
783
932
 
933
+ if (isThinkingTraceMessage(message) && !isThinkingTraceActive(message) && !(message.thinkingEntries?.length > 0)) {
934
+ state.expandedThinkingTraceIds.delete(message.id);
935
+ return;
936
+ }
937
+
784
938
  if (message.isPending) {
785
939
  state.messages.push(message);
786
940
  return;
787
941
  }
788
942
 
789
- if (message.replyTo) {
943
+ if (message.replyTo && !isThinkingTraceMessage(message)) {
790
944
  const repliedIndex = state.messages.findIndex(
791
945
  (item) => item.id === message.replyTo && item.sender === "user",
792
946
  );
@@ -800,6 +954,9 @@ function upsertMessage(message) {
800
954
  (item) => !(item.sender === "assistant" && item.isPending && item.replyTo === message.replyTo),
801
955
  );
802
956
  }
957
+ if (isThinkingTraceMessage(message) && !isThinkingTraceActive(message)) {
958
+ state.expandedThinkingTraceIds.delete(message.id);
959
+ }
803
960
  state.messages.push(message);
804
961
  }
805
962
 
@@ -822,6 +979,9 @@ function renderMessages() {
822
979
 
823
980
  const bubble = document.createElement("div");
824
981
  bubble.className = "message-bubble";
982
+ if (isThinkingTraceMessage(message)) {
983
+ bubble.classList.add("thinking-bubble");
984
+ }
825
985
 
826
986
  const label = document.createElement("span");
827
987
  label.className = "message-label";
@@ -838,7 +998,9 @@ function renderMessages() {
838
998
 
839
999
  const body = document.createElement("div");
840
1000
  body.className = "message-text";
841
- if (message.isPending && message.sender !== "user") {
1001
+ if (isThinkingTraceMessage(message)) {
1002
+ body.append(renderThinkingTrace(message));
1003
+ } else if (message.isPending && message.sender !== "user") {
842
1004
  const pendingLabel = document.createElement("p");
843
1005
  pendingLabel.textContent = t("chat.pendingLabel");
844
1006
  body.append(pendingLabel, createPendingIndicator());
@@ -261,6 +261,11 @@ const BUNDLES = {
261
261
  youLabel: "You",
262
262
  peerLabelFallback: "Participant",
263
263
  pendingLabel: "Thinking…",
264
+ thinkingTraceFallbackTitle: "Thinking",
265
+ thinkingTraceLive: "Live",
266
+ thinkingTraceDone: "Done",
267
+ thinkingTraceFailed: "Failed",
268
+ thinkingTraceSteps: "{count} steps",
264
269
  mutedLabel: "Bot muted",
265
270
  commandSourceOpenclaw: "OpenClaw",
266
271
  commandSourcePlugin: "Plugin",
@@ -527,6 +532,11 @@ const BUNDLES = {
527
532
  youLabel: "你",
528
533
  peerLabelFallback: "成员",
529
534
  pendingLabel: "思考中…",
535
+ thinkingTraceFallbackTitle: "思考过程",
536
+ thinkingTraceLive: "进行中",
537
+ thinkingTraceDone: "已完成",
538
+ thinkingTraceFailed: "失败",
539
+ thinkingTraceSteps: "{count} 步",
530
540
  mutedLabel: "机器人已静音",
531
541
  commandSourceOpenclaw: "OpenClaw",
532
542
  commandSourcePlugin: "插件",
@@ -793,6 +803,11 @@ const BUNDLES = {
793
803
  youLabel: "你",
794
804
  peerLabelFallback: "成員",
795
805
  pendingLabel: "思考中…",
806
+ thinkingTraceFallbackTitle: "思考過程",
807
+ thinkingTraceLive: "進行中",
808
+ thinkingTraceDone: "已完成",
809
+ thinkingTraceFailed: "失敗",
810
+ thinkingTraceSteps: "{count} 步",
796
811
  mutedLabel: "機器人已靜音",
797
812
  commandSourceOpenclaw: "OpenClaw",
798
813
  commandSourcePlugin: "外掛",
@@ -59,6 +59,52 @@ function parseParticipants(value) {
59
59
  }));
60
60
  }
61
61
 
62
+ function parseThinkingStatus(value) {
63
+ switch (value) {
64
+ case "started":
65
+ case "streaming":
66
+ case "completed":
67
+ case "failed":
68
+ return value;
69
+ default:
70
+ return "completed";
71
+ }
72
+ }
73
+
74
+ function parseThinkingEntryKind(value) {
75
+ switch (value) {
76
+ case "thought":
77
+ case "action":
78
+ case "result":
79
+ case "error":
80
+ return value;
81
+ default:
82
+ return null;
83
+ }
84
+ }
85
+
86
+ function parseThinkingEntries(value) {
87
+ if (!Array.isArray(value)) {
88
+ return [];
89
+ }
90
+ return value
91
+ .filter((item) => item && typeof item === "object")
92
+ .flatMap((item) => {
93
+ const kind = parseThinkingEntryKind(item.kind);
94
+ if (!kind) {
95
+ return [];
96
+ }
97
+ return [{
98
+ id: typeof item.id === "string" ? item.id : createMessageId("thinking-entry"),
99
+ kind,
100
+ title: typeof item.title === "string" ? item.title : "",
101
+ text: typeof item.text === "string" ? item.text : "",
102
+ sentAt: normalizeTimestamp(item.sentAt),
103
+ toolName: typeof item.toolName === "string" ? item.toolName : null,
104
+ }];
105
+ });
106
+ }
107
+
62
108
  export class PrivateClawWebSessionClient extends EventTarget {
63
109
  constructor(invite, { identity }) {
64
110
  super();
@@ -263,6 +309,7 @@ export class PrivateClawWebSessionClient extends EventTarget {
263
309
  appVersion: "privateclaw_web/0.1.0",
264
310
  appId: this.identity.appId,
265
311
  deviceLabel: "PrivateClaw Web",
312
+ supportsThinkingTrace: true,
266
313
  ...(this.identity.displayName ? { displayName: this.identity.displayName } : {}),
267
314
  sentAt: new Date().toISOString(),
268
315
  });
@@ -334,6 +381,23 @@ export class PrivateClawWebSessionClient extends EventTarget {
334
381
  });
335
382
  return;
336
383
  }
384
+ case "thinking_message": {
385
+ this.#dispatch("message", {
386
+ message: {
387
+ id: typeof payload.messageId === "string" ? payload.messageId : this.#nextLocalMessageId(),
388
+ sender: "assistant",
389
+ text: "",
390
+ sentAt: normalizeTimestamp(payload.sentAt),
391
+ replyTo: typeof payload.replyTo === "string" ? payload.replyTo : null,
392
+ isPending: false,
393
+ attachments: [],
394
+ thinkingStatus: parseThinkingStatus(payload.status),
395
+ thinkingSummary: typeof payload.summary === "string" ? payload.summary : "",
396
+ thinkingEntries: parseThinkingEntries(payload.entries),
397
+ },
398
+ });
399
+ return;
400
+ }
337
401
  case "participant_message": {
338
402
  const senderAppId = typeof payload.senderAppId === "string" ? payload.senderAppId : "unknown-app";
339
403
  this.#dispatch("message", {
@@ -429,6 +493,7 @@ export class PrivateClawWebSessionClient extends EventTarget {
429
493
  appVersion: "privateclaw_web/0.1.0",
430
494
  appId: this.identity.appId,
431
495
  deviceLabel: "PrivateClaw Web",
496
+ supportsThinkingTrace: true,
432
497
  ...(this.identity.displayName ? { displayName: this.identity.displayName } : {}),
433
498
  sentAt: new Date().toISOString(),
434
499
  });
@@ -984,6 +984,11 @@ a:focus {
984
984
  border-color: rgba(255, 127, 159, 0.28);
985
985
  }
986
986
 
987
+ .message-bubble.thinking-bubble {
988
+ background: linear-gradient(180deg, rgba(143, 125, 255, 0.22), rgba(62, 217, 255, 0.08));
989
+ border-color: rgba(143, 125, 255, 0.24);
990
+ }
991
+
987
992
  .message-text {
988
993
  color: var(--text);
989
994
  line-height: 1.6;
@@ -1020,6 +1025,181 @@ a:focus {
1020
1025
  text-decoration: underline;
1021
1026
  }
1022
1027
 
1028
+ .thinking-trace-card {
1029
+ border: 1px solid rgba(255, 255, 255, 0.08);
1030
+ border-radius: 18px;
1031
+ background: rgba(7, 12, 26, 0.28);
1032
+ padding: 12px;
1033
+ }
1034
+
1035
+ details.thinking-trace-card > summary {
1036
+ list-style: none;
1037
+ cursor: pointer;
1038
+ }
1039
+
1040
+ details.thinking-trace-card > summary::-webkit-details-marker {
1041
+ display: none;
1042
+ }
1043
+
1044
+ .thinking-trace-card.active {
1045
+ border-color: rgba(143, 125, 255, 0.3);
1046
+ }
1047
+
1048
+ .thinking-trace-card.failed {
1049
+ border-color: rgba(255, 127, 159, 0.38);
1050
+ background: rgba(62, 10, 24, 0.26);
1051
+ }
1052
+
1053
+ .thinking-trace-summary {
1054
+ display: flex;
1055
+ flex-direction: column;
1056
+ gap: 10px;
1057
+ }
1058
+
1059
+ .thinking-trace-header {
1060
+ display: flex;
1061
+ align-items: flex-start;
1062
+ justify-content: space-between;
1063
+ gap: 12px;
1064
+ }
1065
+
1066
+ .thinking-trace-header-main {
1067
+ min-width: 0;
1068
+ display: inline-flex;
1069
+ align-items: center;
1070
+ gap: 10px;
1071
+ }
1072
+
1073
+ .thinking-trace-header-main strong {
1074
+ font-size: 0.98rem;
1075
+ }
1076
+
1077
+ .thinking-trace-icon {
1078
+ width: 26px;
1079
+ height: 26px;
1080
+ border-radius: 999px;
1081
+ display: inline-flex;
1082
+ align-items: center;
1083
+ justify-content: center;
1084
+ flex-shrink: 0;
1085
+ font-size: 0.92rem;
1086
+ background: rgba(255, 255, 255, 0.08);
1087
+ color: var(--text);
1088
+ }
1089
+
1090
+ .thinking-trace-icon.thought,
1091
+ .thinking-trace-icon.active {
1092
+ background: rgba(143, 125, 255, 0.2);
1093
+ color: rgba(230, 224, 255, 0.96);
1094
+ }
1095
+
1096
+ .thinking-trace-icon.active {
1097
+ animation: pulse 1.2s ease-in-out infinite;
1098
+ }
1099
+
1100
+ .thinking-trace-icon.action {
1101
+ background: rgba(62, 217, 255, 0.18);
1102
+ color: rgba(204, 247, 255, 0.96);
1103
+ }
1104
+
1105
+ .thinking-trace-icon.result {
1106
+ background: rgba(92, 255, 196, 0.18);
1107
+ color: rgba(218, 255, 241, 0.98);
1108
+ }
1109
+
1110
+ .thinking-trace-icon.error {
1111
+ background: rgba(255, 127, 159, 0.22);
1112
+ color: rgba(255, 230, 237, 0.98);
1113
+ }
1114
+
1115
+ .thinking-trace-preview {
1116
+ margin: 0;
1117
+ color: var(--text-muted);
1118
+ white-space: pre-wrap;
1119
+ }
1120
+
1121
+ .thinking-trace-badges {
1122
+ display: flex;
1123
+ flex-wrap: wrap;
1124
+ justify-content: flex-end;
1125
+ gap: 8px;
1126
+ }
1127
+
1128
+ .thinking-trace-badge {
1129
+ display: inline-flex;
1130
+ align-items: center;
1131
+ gap: 6px;
1132
+ padding: 6px 10px;
1133
+ border-radius: 999px;
1134
+ background: rgba(255, 255, 255, 0.08);
1135
+ color: var(--text-muted);
1136
+ font-size: 0.82rem;
1137
+ font-weight: 600;
1138
+ }
1139
+
1140
+ .thinking-trace-badge.live {
1141
+ background: rgba(143, 125, 255, 0.16);
1142
+ color: rgba(233, 228, 255, 0.96);
1143
+ }
1144
+
1145
+ .thinking-trace-badge.done {
1146
+ background: rgba(92, 255, 196, 0.14);
1147
+ color: rgba(220, 255, 243, 0.98);
1148
+ }
1149
+
1150
+ .thinking-trace-badge.error {
1151
+ background: rgba(255, 127, 159, 0.18);
1152
+ color: rgba(255, 233, 239, 0.98);
1153
+ }
1154
+
1155
+ .thinking-trace-badge.tool {
1156
+ background: rgba(62, 217, 255, 0.16);
1157
+ color: rgba(208, 247, 255, 0.98);
1158
+ }
1159
+
1160
+ .thinking-trace-history {
1161
+ margin-top: 12px;
1162
+ padding-top: 12px;
1163
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
1164
+ display: flex;
1165
+ flex-direction: column;
1166
+ gap: 10px;
1167
+ }
1168
+
1169
+ .thinking-trace-entry {
1170
+ border-radius: 16px;
1171
+ padding: 12px;
1172
+ border: 1px solid rgba(255, 255, 255, 0.08);
1173
+ background: rgba(255, 255, 255, 0.04);
1174
+ }
1175
+
1176
+ .thinking-trace-entry.thought {
1177
+ border-color: rgba(143, 125, 255, 0.2);
1178
+ }
1179
+
1180
+ .thinking-trace-entry.action {
1181
+ border-color: rgba(62, 217, 255, 0.2);
1182
+ }
1183
+
1184
+ .thinking-trace-entry.result {
1185
+ border-color: rgba(92, 255, 196, 0.2);
1186
+ }
1187
+
1188
+ .thinking-trace-entry.error {
1189
+ border-color: rgba(255, 127, 159, 0.24);
1190
+ }
1191
+
1192
+ .thinking-trace-entry-header {
1193
+ display: flex;
1194
+ align-items: center;
1195
+ flex-wrap: wrap;
1196
+ gap: 10px;
1197
+ }
1198
+
1199
+ .thinking-trace-entry-body {
1200
+ margin-top: 8px;
1201
+ }
1202
+
1023
1203
  .mermaid-card {
1024
1204
  border: 1px solid var(--line);
1025
1205
  border-radius: 16px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privateclaw/privateclaw-relay",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Blind WebSocket relay for PrivateClaw encrypted sessions with local CLI startup and optional Tailscale Funnel or Cloudflare Tunnel exposure",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",