@clowder-ai/companion 0.1.0-alpha.25 → 0.1.0-alpha.27

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/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pluginId": "official.companion",
3
- "version": "0.1.0-alpha.25",
3
+ "version": "0.1.0-alpha.27",
4
4
  "contractVersion": "0.1.0",
5
5
  "name": "猫猫球",
6
6
  "icon": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clowder-ai/companion",
3
- "version": "0.1.0-alpha.25",
3
+ "version": "0.1.0-alpha.27",
4
4
  "description": "The desktop voice body for your Clowder companion",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/plugin.yaml CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pluginId": "official.companion",
3
- "version": "0.1.0-alpha.25",
3
+ "version": "0.1.0-alpha.27",
4
4
  "contractVersion": "0.1.0",
5
5
  "name": "猫猫球",
6
6
  "icon": {
@@ -14,9 +14,11 @@ export class ConversationSurface {
14
14
  onAccepted, retryLegacy, status });
15
15
  this.scheduleInterval = (...args) => Reflect.apply(scheduleInterval, globalThis, args);
16
16
  this.cancelInterval = (...args) => Reflect.apply(cancelInterval, globalThis, args);
17
- this.loading = false;
17
+ this.activePageRead = null;
18
+ this.pendingOlderRead = false;
18
19
  this.bodyReads = new Set();
19
20
  this.replyMode = 'idle';
21
+ this.historySelection = null;
20
22
  this.controller = new ReplyLetterController({
21
23
  requestContinuation: value => void this.readBody(value.messageId, value.continuation),
22
24
  render: value => this.renderReply(value),
@@ -58,14 +60,27 @@ export class ConversationSurface {
58
60
  void this.readOlderHistory();
59
61
  }
60
62
  });
63
+ this.$('chat-transcript').addEventListener('wheel', event => {
64
+ if (event.deltaY < 0 && this.controller.mode === 'history'
65
+ && this.$('chat-transcript').scrollTop <= 0) void this.readOlderHistory();
66
+ }, { passive: true });
61
67
  this.$('compose').onsubmit = event => this.submit(event);
62
68
  }
63
69
 
64
70
  renderReply(value) {
65
71
  const previous = this.replyMode;
72
+ const previousSelection = this.historySelection;
73
+ if (previous === 'history' && value.mode !== 'history') {
74
+ this.transcript.leaveHistory(previousSelection);
75
+ }
66
76
  this.replyMode = value.mode;
67
77
  this.view.render(value);
68
78
  this.controls.remeasure?.();
79
+ if (value.mode === 'history'
80
+ && (previous !== 'history' || previousSelection !== value.selectedMessageId)) {
81
+ this.transcript.enterHistory(value.selectedMessageId);
82
+ }
83
+ this.historySelection = value.mode === 'history' ? value.selectedMessageId : null;
69
84
  if (value.mode === 'reply' && previous !== 'reply' && this.controls.panel !== 'chat') {
70
85
  void this.controls.show('chat');
71
86
  }
@@ -116,8 +131,12 @@ export class ConversationSurface {
116
131
  }
117
132
 
118
133
  async readHistory({ before, older = false } = {}) {
119
- if (this.loading) return;
120
- this.loading = true;
134
+ const readKind = older ? 'older' : 'latest';
135
+ if (this.activePageRead) {
136
+ if (readKind === 'older' && this.activePageRead === 'latest') this.pendingOlderRead = true;
137
+ return;
138
+ }
139
+ this.activePageRead = readKind;
121
140
  try {
122
141
  const page = await this.client.readConversationPage(before === undefined ? {} : { before });
123
142
  this.history.loadPage(page, { older });
@@ -130,12 +149,17 @@ export class ConversationSurface {
130
149
  this.controller.clearProtected('permission_required');
131
150
  this.$('chat-connection-status').textContent = '聊天权限已撤销';
132
151
  } else this.$('chat-connection-status').textContent = '聊天记录暂未更新 · 正在说的话仍会显示';
133
- } finally { this.loading = false; }
152
+ } finally {
153
+ const readPendingOlder = readKind === 'latest' && this.pendingOlderRead;
154
+ if (readKind === 'latest') this.pendingOlderRead = false;
155
+ this.activePageRead = null;
156
+ if (readPendingOlder) void this.readOlderHistory();
157
+ }
134
158
  }
135
159
 
136
160
  async readOlderHistory() {
137
- const { hasMoreOlder, olderCursor } = this.history.snapshot();
138
- if (!hasMoreOlder || !olderCursor) return;
161
+ const { hasMoreOlder, olderCursor, pageStatus } = this.history.snapshot();
162
+ if (this.controller.mode !== 'history' || pageStatus !== 'available' || !hasMoreOlder || !olderCursor) return;
139
163
  await this.readHistory({ before: olderCursor, older: true });
140
164
  }
141
165
 
@@ -6,6 +6,8 @@ export class TranscriptView {
6
6
  this.activeRows = new Map();
7
7
  this.historyRows = new Map();
8
8
  this.completed = new Set();
9
+ this.savedHistoryViewport = null;
10
+ this.pendingHistoryEntry = null;
9
11
  }
10
12
  append(role, text, chunk = false) {
11
13
  const pinned = this.nearBottom();
@@ -33,7 +35,8 @@ export class TranscriptView {
33
35
  if (pinned) this.container.scrollTop = this.container.scrollHeight;
34
36
  }
35
37
  nearBottom() {
36
- return this.container.scrollHeight - this.container.scrollTop - this.container.clientHeight < 32;
38
+ return !this.container.hidden
39
+ && this.container.scrollHeight - this.container.scrollTop - this.container.clientHeight < 32;
37
40
  }
38
41
  visibleHistoryAnchor() {
39
42
  const viewport = this.container.getBoundingClientRect?.();
@@ -45,15 +48,75 @@ export class TranscriptView {
45
48
  }
46
49
  return null;
47
50
  }
51
+ restoreHistoryAnchor(anchor, readingPosition, useFirstAvailable = false) {
52
+ const row = this.historyRows.get(anchor?.id)
53
+ ?? (useFirstAvailable ? this.historyRows.values().next().value : null);
54
+ const rect = row?.getBoundingClientRect?.();
55
+ const viewport = this.container.getBoundingClientRect?.();
56
+ if (!rect || !viewport || viewport.bottom <= viewport.top) {
57
+ this.container.scrollTop = readingPosition;
58
+ return false;
59
+ }
60
+ this.container.scrollTop += rect.top - viewport.top - anchor.offset;
61
+ return true;
62
+ }
63
+ revealHistoryRow(id) {
64
+ const row = this.historyRows.get(id);
65
+ const rect = row?.getBoundingClientRect?.();
66
+ const viewport = this.container.getBoundingClientRect?.();
67
+ if (!rect || !viewport || viewport.bottom <= viewport.top) return false;
68
+ if (rect.top >= viewport.top && rect.bottom <= viewport.bottom) return true;
69
+ const viewportHeight = viewport.bottom - viewport.top;
70
+ const rowHeight = Math.max(0, rect.bottom - rect.top);
71
+ const offset = Math.max(0, (viewportHeight - rowHeight) / 2);
72
+ this.container.scrollTop += rect.top - viewport.top - offset;
73
+ return true;
74
+ }
75
+ placeHistoryEntry(afterLoad) {
76
+ if (!this.pendingHistoryEntry || this.container.hidden) return false;
77
+ const viewport = this.pendingHistoryEntry;
78
+ if (viewport.anchor && this.restoreHistoryAnchor(viewport.anchor, viewport.scrollTop)) {
79
+ this.pendingHistoryEntry = null;
80
+ return true;
81
+ }
82
+ if (viewport.selectedMessageId && this.revealHistoryRow(viewport.selectedMessageId)) {
83
+ this.pendingHistoryEntry = null;
84
+ return true;
85
+ }
86
+ const bounds = this.container.getBoundingClientRect?.();
87
+ if (!afterLoad || !bounds || bounds.bottom <= bounds.top) return false;
88
+ this.container.scrollTop = this.container.scrollHeight;
89
+ this.pendingHistoryEntry = null;
90
+ return true;
91
+ }
92
+ leaveHistory(selectedMessageId) {
93
+ this.savedHistoryViewport = {
94
+ selectedMessageId,
95
+ anchor: this.visibleHistoryAnchor(),
96
+ scrollTop: this.container.scrollTop,
97
+ };
98
+ this.pendingHistoryEntry = null;
99
+ }
100
+ enterHistory(selectedMessageId) {
101
+ const saved = this.savedHistoryViewport?.selectedMessageId === selectedMessageId
102
+ ? this.savedHistoryViewport : null;
103
+ this.pendingHistoryEntry = {
104
+ selectedMessageId,
105
+ anchor: saved?.anchor ?? null,
106
+ scrollTop: saved?.scrollTop ?? this.container.scrollTop,
107
+ };
108
+ this.placeHistoryEntry(false);
109
+ }
48
110
  reset() {
49
111
  for (const row of this.activeRows.values()) row.remove();
50
112
  this.activeRows.clear();
51
113
  this.completed.clear();
52
114
  }
53
115
  load(messages) {
54
- const pinned = this.nearBottom();
116
+ const hidden = this.container.hidden;
117
+ const pinned = !hidden && this.nearBottom();
55
118
  const readingPosition = this.container.scrollTop;
56
- const anchor = pinned ? null : this.visibleHistoryAnchor();
119
+ const anchor = hidden || pinned ? null : this.visibleHistoryAnchor();
57
120
  const previous = this.historyRows;
58
121
  this.historyRows = new Map();
59
122
  const rows = messages.map(message => {
@@ -81,14 +144,10 @@ export class TranscriptView {
81
144
  return row;
82
145
  });
83
146
  this.container.replaceChildren(...rows, ...this.activeRows.values());
84
- if (pinned) this.container.scrollTop = this.container.scrollHeight;
85
- else if (anchor) {
86
- const row = this.historyRows.get(anchor.id) ?? rows[0];
87
- const rect = row?.getBoundingClientRect?.();
88
- const viewport = this.container.getBoundingClientRect?.();
89
- this.container.scrollTop = rect && viewport
90
- ? this.container.scrollTop + rect.top - viewport.top - anchor.offset
91
- : readingPosition;
92
- } else this.container.scrollTop = readingPosition;
147
+ if (hidden) this.container.scrollTop = readingPosition;
148
+ else if (this.placeHistoryEntry(true)) return;
149
+ else if (pinned) this.container.scrollTop = this.container.scrollHeight;
150
+ else if (anchor) this.restoreHistoryAnchor(anchor, readingPosition, true);
151
+ else this.container.scrollTop = readingPosition;
93
152
  }
94
153
  }