@awiki/dsh-plugin 0.2.1 → 0.2.2

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
@@ -19,7 +19,7 @@ TypeScript SDK `identity.json`; create a new Rust-backed identity after upgradin
19
19
  - Register one deployment-level AWiki identity from the Web UI.
20
20
  - Open the top-left AWiki account menu to sign out locally without deleting the encrypted identity or message database; **Resume** restores the same DID and Handle, including across DSH restarts.
21
21
  - Reuse that identity across the root Agent and its subagents.
22
- - Direct-message and existing-group conversation lists, unread counts, latest-message previews, and persisted display names. Opening a conversation keeps loading feedback inside the history and lands on the newest message; scrolling up reveals a latest-message control that counts newer arrivals without interrupting reading.
22
+ - Direct-message and existing-group conversation lists, unread counts, latest-message previews, and persisted display names. Opening a conversation keeps loading feedback inside the history and lands on the newest message; scrolling up reveals a latest-message control that counts newer arrivals without interrupting reading. A conversation is marked read only after its newest rendered message reaches the visible bottom.
23
23
  - Text messages plus one attachment per message, with Enter-to-send, Shift+Enter line breaks, optimistic sending bubbles, image previews, and SHA verification.
24
24
  - A draggable circular launcher, adaptive popup placement, dark mode, and remembered active conversation.
25
25
  - User-triggered AI summaries for up to 50 recent or unread messages, kept only in runtime memory with explicit stale, retry, copy, and source-navigation states.
@@ -36,7 +36,7 @@ group administration, realtime push, or multiple attachments in one message.
36
36
  Install the official public npm package:
37
37
 
38
38
  ```bash
39
- pnpm add @awiki/dsh-plugin@next
39
+ pnpm add @awiki/dsh-plugin@latest
40
40
  ```
41
41
 
42
42
  `@awiki/dsh-plugin` is the canonical package identity starting with
package/README.zh.md CHANGED
@@ -13,7 +13,7 @@ Rust 身份。
13
13
  - 在 Web UI 中注册一个部署级 AWiki 身份,根 Agent 与子 Agent 共用。
14
14
  - 点击 AWiki 面板左上角图标可打开账户菜单;普通退出只锁定本机会话,不删除加密身份或消息数据库,重新进入及重启 DSH 后仍恢复同一个 DID 和 Handle。
15
15
  - 注册失败时保留手机号、Handle、验证码和本机待注册密钥;注册未开放、验证码状态失效和提交冲突会给出对应的安全处理提示。
16
- - 私聊和已有群聊列表、未读角标、最新消息预览、时间更新与昵称持久化。打开会话时在消息区内显示加载状态并落到最新消息;向上阅读时显示下滑箭头,新消息到达后在同一控件中累计数量且不打断阅读位置。
16
+ - 私聊和已有群聊列表、未读角标、最新消息预览、时间更新与昵称持久化。打开会话时在消息区内显示加载状态并落到最新消息;向上阅读时显示下滑箭头,新消息到达后在同一控件中累计数量且不打断阅读位置。只有最新一条已渲染消息到达可视区域底部后,当前会话才会自动标记为已读。
17
17
  - 文本和单附件消息;Enter 发送、Shift+Enter 换行,发送中立即显示带 loading 动画的乐观气泡,并支持图片预览、附件说明与 SHA 校验。
18
18
  - 圆形可拖动入口、自适应四角弹窗、深色模式和当前会话记忆。
19
19
  - 用户点击后才生成的 AI 对话总结:最多处理 50 条最近或未读消息,按会话保留本次运行期缓存,并支持过期提示、重试、复制与跳转原消息。
@@ -29,7 +29,7 @@ Rust 身份。
29
29
  安装公开发布的官方 npm 包:
30
30
 
31
31
  ```bash
32
- pnpm add @awiki/dsh-plugin@next
32
+ pnpm add @awiki/dsh-plugin@latest
33
33
  ```
34
34
 
35
35
  从 `0.2.0-rc.4` 起,`@awiki/dsh-plugin` 是唯一规范包名。原
package/lib/client.js CHANGED
@@ -5451,6 +5451,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5451
5451
  generation = 0;
5452
5452
  disposed = false;
5453
5453
  polling = false;
5454
+ markReadInFlight = /* @__PURE__ */ new Map();
5454
5455
  unreadAtOpen = /* @__PURE__ */ new Map();
5455
5456
  summaryBaselines = /* @__PURE__ */ new Map();
5456
5457
  /** @param remote - generated Host Remote namespace. */
@@ -5560,6 +5561,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5560
5561
  if (this.timer !== void 0) clearInterval(this.timer);
5561
5562
  this.timer = void 0;
5562
5563
  this.polling = false;
5564
+ this.markReadInFlight.clear();
5563
5565
  }
5564
5566
  /**
5565
5567
  * Request one phone verification challenge.
@@ -5716,24 +5718,51 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5716
5718
  })
5717
5719
  });
5718
5720
  if (!loaded.ok) return loaded;
5719
- const marked = await call(() => this.remote.markConversationRead({ conversationId }));
5720
- if (!marked.ok) return this.fail(marked.error);
5721
- if (!this.current(generation) || this.view.selectedConversationId !== conversationId) return {
5721
+ return {
5722
5722
  ok: true,
5723
5723
  value: void 0
5724
5724
  };
5725
- this.publish({
5726
- ...this.view,
5727
- conversations: this.view.conversations.map((conversation) => conversation.id === conversationId ? {
5728
- ...conversation,
5729
- unreadCount: 0
5730
- } : conversation),
5731
- error: null
5732
- });
5733
- return {
5725
+ }
5726
+ /**
5727
+ * Mark the selected conversation read after the UI proves its newest message is visible.
5728
+ * Repeated scroll and layout notifications share one Host request, while a failed
5729
+ * background attempt keeps the unread badge so reaching the bottom can retry.
5730
+ */
5731
+ async markSelectedConversationRead() {
5732
+ if (this.disposed) return {
5733
+ ok: false,
5734
+ error: "AWiki 插件已卸载"
5735
+ };
5736
+ const conversation = this.selectedConversation();
5737
+ if (conversation === void 0 || (conversation.unreadCount ?? 0) <= 0) return {
5734
5738
  ok: true,
5735
5739
  value: void 0
5736
5740
  };
5741
+ const existing = this.markReadInFlight.get(conversation.id);
5742
+ if (existing !== void 0) return existing;
5743
+ const conversationId = conversation.id;
5744
+ const generation = this.generation;
5745
+ const operation = (async () => {
5746
+ const result = await call(() => this.remote.markConversationRead({ conversationId }));
5747
+ if (!result.ok) return result;
5748
+ if (this.current(generation)) this.publish({
5749
+ ...this.view,
5750
+ conversations: this.view.conversations.map((current) => current.id === conversationId ? {
5751
+ ...current,
5752
+ unreadCount: 0
5753
+ } : current)
5754
+ });
5755
+ return {
5756
+ ok: true,
5757
+ value: void 0
5758
+ };
5759
+ })();
5760
+ this.markReadInFlight.set(conversationId, operation);
5761
+ try {
5762
+ return await operation;
5763
+ } finally {
5764
+ if (this.markReadInFlight.get(conversationId) === operation) this.markReadInFlight.delete(conversationId);
5765
+ }
5737
5766
  }
5738
5767
  /**
5739
5768
  * Load one older history page before the currently rendered messages.
@@ -6991,6 +7020,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6991
7020
  const summaryPanelId = (0, react.useId)();
6992
7021
  selectedConversationId.current = view.selectedConversationId;
6993
7022
  const visibleSendingDraft = sendingDraft?.conversationId === view.selectedConversationId ? sendingDraft : null;
7023
+ const markSelectedConversationReadAtBottom = () => {
7024
+ const node = history.current;
7025
+ const newestRendered = view.messages.at(-1);
7026
+ if (node === null || selected === void 0 || newestRendered === void 0 || (selected.unreadCount ?? 0) <= 0 || view.pending === "加载消息" || selected.lastMessageAt !== void 0 && newestRendered.sentAt < selected.lastMessageAt || node.scrollHeight - node.scrollTop - node.clientHeight > HISTORY_BOTTOM_THRESHOLD) return;
7027
+ props.markSelectedConversationRead();
7028
+ };
6994
7029
  const scrollHistoryToLatest = (smooth) => {
6995
7030
  const node = history.current;
6996
7031
  if (node === null) return;
@@ -7003,6 +7038,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7003
7038
  historyPinnedToBottom.current = true;
7004
7039
  setHistoryAwayFromBottom(false);
7005
7040
  setUnseenMessageCount(0);
7041
+ if (!smooth || reduceMotion || typeof node.scrollTo !== "function") markSelectedConversationReadAtBottom();
7006
7042
  };
7007
7043
  const syncHistoryPosition = () => {
7008
7044
  const node = history.current;
@@ -7010,8 +7046,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7010
7046
  const atBottom = node.scrollHeight - node.scrollTop - node.clientHeight <= HISTORY_BOTTOM_THRESHOLD;
7011
7047
  historyPinnedToBottom.current = atBottom;
7012
7048
  setHistoryAwayFromBottom(!atBottom);
7013
- if (atBottom) setUnseenMessageCount(0);
7049
+ if (atBottom) {
7050
+ setUnseenMessageCount(0);
7051
+ markSelectedConversationReadAtBottom();
7052
+ }
7014
7053
  };
7054
+ (0, react.useLayoutEffect)(() => {
7055
+ markSelectedConversationReadAtBottom();
7056
+ }, [
7057
+ selected?.id,
7058
+ selected?.lastMessageAt,
7059
+ selected?.unreadCount,
7060
+ view.messages,
7061
+ view.pending
7062
+ ]);
7015
7063
  (0, react.useLayoutEffect)(() => {
7016
7064
  const conversationId = view.selectedConversationId;
7017
7065
  if (conversationId !== previousConversationId.current) {
@@ -7036,7 +7084,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7036
7084
  messageId: view.messages.at(-1)?.id ?? null
7037
7085
  };
7038
7086
  scrollHistoryToLatest(false);
7039
- if (pendingInitialImages.current.size === 0) conversationAwaitingBottom.current = null;
7087
+ if (pendingInitialImages.current.size === 0) {
7088
+ conversationAwaitingBottom.current = null;
7089
+ markSelectedConversationReadAtBottom();
7090
+ }
7040
7091
  return;
7041
7092
  }
7042
7093
  const previous = previousMessageTail.current;
@@ -7067,6 +7118,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7067
7118
  if (!pendingInitialImages.current.delete(messageId)) return;
7068
7119
  if (history.current !== null) scrollHistoryToLatest(false);
7069
7120
  if (pendingInitialImages.current.size === 0) conversationAwaitingBottom.current = null;
7121
+ if (pendingInitialImages.current.size === 0) markSelectedConversationReadAtBottom();
7070
7122
  };
7071
7123
  const viewSummarySource = (messageId) => {
7072
7124
  if (selected === void 0) return;
@@ -8522,6 +8574,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8522
8574
  loadMoreConversations: () => controller.loadMoreConversations(),
8523
8575
  startDirectChat: (handle) => controller.startDirectChat(handle),
8524
8576
  selectConversation: (conversationId) => controller.selectConversation(conversationId),
8577
+ markSelectedConversationRead: () => controller.markSelectedConversationRead(),
8525
8578
  loadOlderHistory: () => controller.loadOlderHistory(),
8526
8579
  summarizeConversation: () => controller.summarizeConversation(),
8527
8580
  setSummaryCollapsed: (conversationId, collapsed) => {