@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 +1 -1
- package/package.json +1 -1
- package/plugin.yaml +1 -1
- package/renderer/conversation-surface.mjs +14 -0
- package/renderer/transcript-view.mjs +71 -12
package/manifest.json
CHANGED
package/package.json
CHANGED
package/plugin.yaml
CHANGED
|
@@ -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.
|
|
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
|
}
|