@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 +1 -1
- package/package.json +1 -1
- package/plugin.yaml +1 -1
- package/renderer/conversation-surface.mjs +30 -6
- package/renderer/transcript-view.mjs +71 -12
package/manifest.json
CHANGED
package/package.json
CHANGED
package/plugin.yaml
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
120
|
-
this.
|
|
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 {
|
|
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.
|
|
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
|
|
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 (
|
|
85
|
-
else if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
}
|