@clowder-ai/companion 0.1.0-alpha.26 → 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.26",
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.26",
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.26",
3
+ "version": "0.1.0-alpha.27",
4
4
  "contractVersion": "0.1.0",
5
5
  "name": "猫猫球",
6
6
  "icon": {
@@ -18,6 +18,7 @@ export class ConversationSurface {
18
18
  this.pendingOlderRead = false;
19
19
  this.bodyReads = new Set();
20
20
  this.replyMode = 'idle';
21
+ this.historySelection = null;
21
22
  this.controller = new ReplyLetterController({
22
23
  requestContinuation: value => void this.readBody(value.messageId, value.continuation),
23
24
  render: value => this.renderReply(value),
@@ -59,14 +60,27 @@ export class ConversationSurface {
59
60
  void this.readOlderHistory();
60
61
  }
61
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 });
62
67
  this.$('compose').onsubmit = event => this.submit(event);
63
68
  }
64
69
 
65
70
  renderReply(value) {
66
71
  const previous = this.replyMode;
72
+ const previousSelection = this.historySelection;
73
+ if (previous === 'history' && value.mode !== 'history') {
74
+ this.transcript.leaveHistory(previousSelection);
75
+ }
67
76
  this.replyMode = value.mode;
68
77
  this.view.render(value);
69
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;
70
84
  if (value.mode === 'reply' && previous !== 'reply' && this.controls.panel !== 'chat') {
71
85
  void this.controls.show('chat');
72
86
  }
@@ -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
  }