@hahnfeld/teams-adapter 1.4.1 → 1.5.0
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/dist/adapter.d.ts +2 -19
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +93 -236
- package/dist/adapter.js.map +1 -1
- package/dist/commands/new-session.d.ts.map +1 -1
- package/dist/commands/new-session.js +68 -11
- package/dist/commands/new-session.js.map +1 -1
- package/dist/commands/session.d.ts +1 -2
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +8 -12
- package/dist/commands/session.js.map +1 -1
- package/dist/message-composer.d.ts +66 -58
- package/dist/message-composer.d.ts.map +1 -1
- package/dist/message-composer.js +301 -216
- package/dist/message-composer.js.map +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +54 -98
- package/dist/permissions.js.map +1 -1
- package/package.json +1 -1
package/dist/message-composer.js
CHANGED
|
@@ -1,30 +1,93 @@
|
|
|
1
1
|
import { log } from "@openacp/plugin-sdk";
|
|
2
2
|
import { CardFactory } from "@microsoft/agents-hosting";
|
|
3
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
3
4
|
const MAX_ROOT_TEXT_LENGTH = 25_000;
|
|
4
|
-
/** How long to wait with no activity before warning about truncation. */
|
|
5
5
|
const STALL_TIMEOUT = 120_000;
|
|
6
|
-
// ───
|
|
7
|
-
// Primary blue #01426a | Dark blue #00274a | Purple #463c8f
|
|
8
|
-
// Cyan #5de3f7 | Green #a3cd6a | Magenta #ce0c88
|
|
9
|
-
// Text default #2a2a2a | Muted #676767 | White #ffffff
|
|
10
|
-
const BRAND = {
|
|
11
|
-
blue: "#01426a",
|
|
12
|
-
blueDark: "#00274a",
|
|
13
|
-
purple: "#463c8f",
|
|
14
|
-
cyan: "#5de3f7",
|
|
15
|
-
green: "#a3cd6a",
|
|
16
|
-
magenta: "#ce0c88",
|
|
17
|
-
textDefault: "#2a2a2a",
|
|
18
|
-
textMuted: "#676767",
|
|
19
|
-
white: "#ffffff",
|
|
20
|
-
surfaceMuted: "#f7f7f7",
|
|
21
|
-
};
|
|
22
|
-
// ─── ID generation ─────────────────────────────────────────────────────────────
|
|
6
|
+
// ─── ID generation ────────────────────────────────────────────────────────────
|
|
23
7
|
let _idCounter = 0;
|
|
24
8
|
function nextId() {
|
|
25
9
|
return `e${++_idCounter}_${Date.now().toString(36)}`;
|
|
26
10
|
}
|
|
11
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
12
|
+
function formatElapsed(ms) {
|
|
13
|
+
if (ms < 1000)
|
|
14
|
+
return `${ms}ms`;
|
|
15
|
+
if (ms < 60_000)
|
|
16
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
17
|
+
return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1000)}s`;
|
|
18
|
+
}
|
|
19
|
+
export function escapeMd(text) {
|
|
20
|
+
return text.replace(/\[/g, "\\[").replace(/\*/g, "\\*").replace(/\]/g, "\\]");
|
|
21
|
+
}
|
|
22
|
+
const PLAN_STATUS_ICONS = {
|
|
23
|
+
completed: "✅",
|
|
24
|
+
in_progress: "🔄",
|
|
25
|
+
pending: "⏳",
|
|
26
|
+
};
|
|
27
27
|
// ─── Adaptive Card Builder ────────────────────────────────────────────────────
|
|
28
|
+
/**
|
|
29
|
+
* Build a 2-column ColumnSet for level-1 headings.
|
|
30
|
+
* Column 1: emoji (auto), Column 2: text (stretch, wrap).
|
|
31
|
+
* Ensures long text wraps without breaking back to the left margin.
|
|
32
|
+
*/
|
|
33
|
+
export function buildLevel1(emoji, text) {
|
|
34
|
+
return {
|
|
35
|
+
type: "ColumnSet",
|
|
36
|
+
spacing: "None",
|
|
37
|
+
columns: [
|
|
38
|
+
{
|
|
39
|
+
type: "Column",
|
|
40
|
+
width: "auto",
|
|
41
|
+
items: [{ type: "TextBlock", text: emoji, size: "Small", fontType: "Monospace", spacing: "None" }],
|
|
42
|
+
verticalContentAlignment: "Top",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: "Column",
|
|
46
|
+
width: "stretch",
|
|
47
|
+
items: [{ type: "TextBlock", text, size: "Small", fontType: "Monospace", wrap: true, spacing: "None" }],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build a 3-column ColumnSet for indented level-2 content.
|
|
54
|
+
* Column 1: spacer (20px), Column 2: ⎿ (auto), Column 3: content (stretch, wrap).
|
|
55
|
+
*/
|
|
56
|
+
export function buildLevel2(text, elapsed, raw = false) {
|
|
57
|
+
const escaped = raw ? text : escapeMd(text);
|
|
58
|
+
const content = elapsed ? `${escaped} (${elapsed})` : escaped;
|
|
59
|
+
return {
|
|
60
|
+
type: "ColumnSet",
|
|
61
|
+
spacing: "None",
|
|
62
|
+
columns: [
|
|
63
|
+
{ type: "Column", width: "20px" },
|
|
64
|
+
{
|
|
65
|
+
type: "Column",
|
|
66
|
+
width: "auto",
|
|
67
|
+
items: [{
|
|
68
|
+
type: "TextBlock",
|
|
69
|
+
text: "⎿",
|
|
70
|
+
size: "Small",
|
|
71
|
+
fontType: "Monospace",
|
|
72
|
+
spacing: "None",
|
|
73
|
+
}],
|
|
74
|
+
verticalContentAlignment: "Top",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: "Column",
|
|
78
|
+
width: "stretch",
|
|
79
|
+
items: [{
|
|
80
|
+
type: "TextBlock",
|
|
81
|
+
text: content,
|
|
82
|
+
size: "Small",
|
|
83
|
+
fontType: "Monospace",
|
|
84
|
+
wrap: true,
|
|
85
|
+
spacing: "None",
|
|
86
|
+
}],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
28
91
|
function buildCardBody(entries) {
|
|
29
92
|
const blocks = [];
|
|
30
93
|
const now = Date.now();
|
|
@@ -40,45 +103,28 @@ function buildCardBody(entries) {
|
|
|
40
103
|
spacing: "None",
|
|
41
104
|
});
|
|
42
105
|
break;
|
|
43
|
-
case "
|
|
106
|
+
case "timed": {
|
|
44
107
|
const elapsed = entry.result
|
|
45
108
|
? formatElapsed((entry.endedAt ?? entry.startedAt) - entry.startedAt)
|
|
46
109
|
: formatElapsed(now - entry.startedAt);
|
|
110
|
+
const headingText = entry.result
|
|
111
|
+
? escapeMd(entry.label)
|
|
112
|
+
: `${escapeMd(entry.label)}… (${elapsed})`;
|
|
113
|
+
const items = [buildLevel1(entry.emoji, headingText)];
|
|
114
|
+
if (entry.result) {
|
|
115
|
+
items.push(buildLevel2(entry.result, elapsed));
|
|
116
|
+
}
|
|
117
|
+
blocks.push({ type: "Container", items, spacing: "Small" });
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case "info": {
|
|
47
121
|
blocks.push({
|
|
48
122
|
type: "Container",
|
|
49
123
|
items: [
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
text: entry.result
|
|
53
|
-
? `🔧 ${escapeMd(entry.toolName)}`
|
|
54
|
-
: `🔧 ${escapeMd(entry.toolName)}… (${elapsed})`,
|
|
55
|
-
size: "Small",
|
|
56
|
-
fontType: "Monospace",
|
|
57
|
-
spacing: "None",
|
|
58
|
-
},
|
|
59
|
-
// Result: indented with L-shaped bar
|
|
60
|
-
...(entry.result
|
|
61
|
-
? [{
|
|
62
|
-
type: "TextBlock",
|
|
63
|
-
text: `\u00A0\u00A0⎿ ${escapeMd(entry.result)} (${elapsed})`,
|
|
64
|
-
size: "Small",
|
|
65
|
-
fontType: "Monospace",
|
|
66
|
-
wrap: true,
|
|
67
|
-
spacing: "None",
|
|
68
|
-
}]
|
|
69
|
-
: []),
|
|
70
|
-
// Children: further indented
|
|
71
|
-
...entry.children.map((c) => ({
|
|
72
|
-
type: "TextBlock",
|
|
73
|
-
text: `\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${c.text}`,
|
|
74
|
-
size: "Small",
|
|
75
|
-
fontType: "Monospace",
|
|
76
|
-
wrap: true,
|
|
77
|
-
spacing: "None",
|
|
78
|
-
})),
|
|
124
|
+
buildLevel1(entry.emoji, escapeMd(entry.label)),
|
|
125
|
+
buildLevel2(entry.detail),
|
|
79
126
|
],
|
|
80
|
-
|
|
81
|
-
width: "stretch",
|
|
127
|
+
spacing: "Small",
|
|
82
128
|
});
|
|
83
129
|
break;
|
|
84
130
|
}
|
|
@@ -92,11 +138,22 @@ function buildCardBody(entries) {
|
|
|
92
138
|
spacing: "None",
|
|
93
139
|
});
|
|
94
140
|
break;
|
|
95
|
-
case "
|
|
141
|
+
case "plan": {
|
|
142
|
+
const lines = entry.entries.map((e, i) => `${PLAN_STATUS_ICONS[e.status] || "⏳"} ${i + 1}. ${escapeMd(e.content)}`);
|
|
143
|
+
blocks.push({
|
|
144
|
+
type: "TextBlock",
|
|
145
|
+
text: `📋 Plan\n${lines.join("\n")}`,
|
|
146
|
+
size: "Small",
|
|
147
|
+
fontType: "Monospace",
|
|
148
|
+
wrap: true,
|
|
149
|
+
spacing: "Small",
|
|
150
|
+
});
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "resource":
|
|
96
154
|
blocks.push({
|
|
97
155
|
type: "TextBlock",
|
|
98
156
|
text: entry.text,
|
|
99
|
-
italic: true,
|
|
100
157
|
size: "Small",
|
|
101
158
|
fontType: "Monospace",
|
|
102
159
|
wrap: true,
|
|
@@ -107,7 +164,7 @@ function buildCardBody(entries) {
|
|
|
107
164
|
blocks.push({
|
|
108
165
|
type: "TextBlock",
|
|
109
166
|
text: `*${escapeMd(entry.text)}*`,
|
|
110
|
-
|
|
167
|
+
isSubtle: true,
|
|
111
168
|
size: "Small",
|
|
112
169
|
fontType: "Monospace",
|
|
113
170
|
spacing: "None",
|
|
@@ -126,17 +183,9 @@ function buildCardBody(entries) {
|
|
|
126
183
|
}
|
|
127
184
|
return blocks;
|
|
128
185
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (ms < 60_000)
|
|
133
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
134
|
-
return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1000)}s`;
|
|
135
|
-
}
|
|
136
|
-
function escapeMd(text) {
|
|
137
|
-
return text.replace(/\[/g, "\\[").replace(/\*/g, "\\*").replace(/\]/g, "\\]");
|
|
138
|
-
}
|
|
139
|
-
// ─── SessionMessage ────────────────────────────────────────────────────────────
|
|
186
|
+
// ─── SessionMessage ───────────────────────────────────────────────────────────
|
|
187
|
+
/** Dot animation frames: Working. → Working.. → Working... → Working.. → repeat */
|
|
188
|
+
const WORKING_FRAMES = ["Working.", "Working..", "Working...", "Working.."];
|
|
140
189
|
export class SessionMessage {
|
|
141
190
|
context;
|
|
142
191
|
conversationId;
|
|
@@ -146,19 +195,31 @@ export class SessionMessage {
|
|
|
146
195
|
entries = [];
|
|
147
196
|
titleId = null;
|
|
148
197
|
usageId = null;
|
|
149
|
-
|
|
150
|
-
|
|
198
|
+
planId = null;
|
|
199
|
+
thinkingActive = null;
|
|
200
|
+
thinkingText = "";
|
|
201
|
+
working = true;
|
|
202
|
+
workingFrame = 0;
|
|
203
|
+
destroyed = false;
|
|
151
204
|
ref = null;
|
|
152
205
|
lastSent = "";
|
|
153
206
|
stallTimer;
|
|
154
|
-
/** Interval handle for periodic elapsed-time updates on running tools */
|
|
155
207
|
tickInterval;
|
|
208
|
+
emptyCardTimer;
|
|
156
209
|
constructor(context, conversationId, sessionId, rateLimiter, acquireBotToken) {
|
|
157
210
|
this.context = context;
|
|
158
211
|
this.conversationId = conversationId;
|
|
159
212
|
this.sessionId = sessionId;
|
|
160
213
|
this.rateLimiter = rateLimiter;
|
|
161
214
|
this.acquireBotToken = acquireBotToken;
|
|
215
|
+
// Start the working animation immediately
|
|
216
|
+
this.usageId = nextId();
|
|
217
|
+
this.entries.push({ id: this.usageId, kind: "usage", text: WORKING_FRAMES[0] });
|
|
218
|
+
this.startTickInterval();
|
|
219
|
+
// If no real content arrives within 30s, delete the empty card
|
|
220
|
+
this.emptyCardTimer = setTimeout(() => this.deleteIfEmpty(), 30_000);
|
|
221
|
+
if (this.emptyCardTimer.unref)
|
|
222
|
+
this.emptyCardTimer.unref();
|
|
162
223
|
}
|
|
163
224
|
updateContext(context) {
|
|
164
225
|
this.context = context;
|
|
@@ -176,9 +237,46 @@ export class SessionMessage {
|
|
|
176
237
|
const usage = this.entries.find((e) => e.kind === "usage");
|
|
177
238
|
return usage ? usage.text : null;
|
|
178
239
|
}
|
|
179
|
-
// ───
|
|
180
|
-
/**
|
|
240
|
+
// ─── Empty card cleanup ──────────────────────────────────────────────────
|
|
241
|
+
/** Cancel the empty-card timer (called when real content arrives). */
|
|
242
|
+
cancelEmptyCardTimer() {
|
|
243
|
+
if (this.emptyCardTimer) {
|
|
244
|
+
clearTimeout(this.emptyCardTimer);
|
|
245
|
+
this.emptyCardTimer = undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** If no real content was added, delete the card activity and clean up. */
|
|
249
|
+
async deleteIfEmpty() {
|
|
250
|
+
this.emptyCardTimer = undefined;
|
|
251
|
+
if (this.entries.some((e) => e.kind !== "usage"))
|
|
252
|
+
return;
|
|
253
|
+
this.destroyed = true;
|
|
254
|
+
// Delete the card activity if it was sent
|
|
255
|
+
if (this.ref) {
|
|
256
|
+
const token = await this.acquireBotToken();
|
|
257
|
+
// Re-check after await — content may have arrived while token was fetched
|
|
258
|
+
if (this.entries.some((e) => e.kind !== "usage")) {
|
|
259
|
+
this.destroyed = false;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (token) {
|
|
263
|
+
const url = `${this.ref.serviceUrl}/v3/conversations/${encodeURIComponent(this.ref.conversationId)}/activities/${encodeURIComponent(this.ref.activityId)}`;
|
|
264
|
+
try {
|
|
265
|
+
await fetch(url, { method: "DELETE", headers: { "Authorization": `Bearer ${token}` } });
|
|
266
|
+
}
|
|
267
|
+
catch { /* best effort */ }
|
|
268
|
+
}
|
|
269
|
+
this.ref = null;
|
|
270
|
+
}
|
|
271
|
+
this.stopTickInterval();
|
|
272
|
+
this.working = false;
|
|
273
|
+
this.entries = [];
|
|
274
|
+
log.debug({ sessionId: this.sessionId }, "[SessionMessage] Empty card deleted after timeout");
|
|
275
|
+
}
|
|
276
|
+
// ─── Entry API ──────────────────────────────────────────────────────────
|
|
277
|
+
/** Set the persistent session title (bold, always first entry). */
|
|
181
278
|
setTitle(text) {
|
|
279
|
+
this.cancelEmptyCardTimer();
|
|
182
280
|
if (this.titleId) {
|
|
183
281
|
const entry = this.findEntry(this.titleId);
|
|
184
282
|
if (entry && entry.kind === "title")
|
|
@@ -191,59 +289,94 @@ export class SessionMessage {
|
|
|
191
289
|
this.requestFlush();
|
|
192
290
|
}
|
|
193
291
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
292
|
+
* Start a timed entry (tool or thinking). Shows emoji + label with live elapsed timer.
|
|
293
|
+
* Returns the entry ID for pairing with addTimedResult().
|
|
196
294
|
*/
|
|
197
|
-
|
|
295
|
+
addTimedStart(emoji, label) {
|
|
296
|
+
this.cancelEmptyCardTimer();
|
|
198
297
|
const id = nextId();
|
|
199
|
-
|
|
200
|
-
this.entries.push({ id, kind: "tool", toolName, startedAt, children: [] });
|
|
201
|
-
this.toolActive = id;
|
|
298
|
+
this.entries.push({ id, kind: "timed", emoji, label, startedAt: Date.now() });
|
|
202
299
|
this.resetStallTimer();
|
|
203
300
|
this.startTickInterval();
|
|
204
301
|
this.requestFlush();
|
|
205
302
|
return id;
|
|
206
303
|
}
|
|
207
304
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
305
|
+
* Close a timed entry by setting its result. Stops the tick interval if no
|
|
306
|
+
* other timed entries are still running.
|
|
210
307
|
*/
|
|
211
|
-
|
|
212
|
-
const entry = this.
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
308
|
+
addTimedResult(id, result) {
|
|
309
|
+
const entry = this.findEntry(id);
|
|
310
|
+
if (entry && entry.kind === "timed") {
|
|
311
|
+
entry.result = result;
|
|
312
|
+
entry.endedAt = Date.now();
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// Orphan result — create a standalone completed entry
|
|
316
|
+
this.entries.push({ id: nextId(), kind: "timed", emoji: "🔧", label: result, startedAt: Date.now(), result, endedAt: Date.now() });
|
|
317
|
+
}
|
|
318
|
+
const hasRunning = this.entries.some((e) => e.kind === "timed" && !e.result);
|
|
319
|
+
if (!hasRunning)
|
|
218
320
|
this.stopTickInterval();
|
|
219
|
-
|
|
220
|
-
|
|
321
|
+
this.requestFlush();
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Start or accumulate thinking text. The first call creates a timed entry;
|
|
325
|
+
* subsequent calls append to the pending result text. Call closeActiveThinking()
|
|
326
|
+
* to finalize.
|
|
327
|
+
*/
|
|
328
|
+
addThinking(text) {
|
|
329
|
+
this.cancelEmptyCardTimer();
|
|
330
|
+
if (!this.thinkingActive) {
|
|
331
|
+
const id = this.addTimedStart("☁️", "Thinking");
|
|
332
|
+
this.thinkingActive = id;
|
|
333
|
+
this.thinkingText = text;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
this.thinkingText += ` ${text}`;
|
|
221
337
|
}
|
|
222
|
-
entry.result = result;
|
|
223
|
-
entry.endedAt = Date.now();
|
|
224
|
-
this.toolActive = null; // Subsequent text goes to root level
|
|
225
|
-
this.stopTickInterval();
|
|
226
338
|
this.requestFlush();
|
|
227
339
|
}
|
|
228
|
-
/**
|
|
229
|
-
|
|
230
|
-
|
|
340
|
+
/**
|
|
341
|
+
* Close the active thinking entry (if any). Called by non-thought handlers
|
|
342
|
+
* so thinking ends when the next event type arrives.
|
|
343
|
+
*/
|
|
344
|
+
closeActiveThinking() {
|
|
345
|
+
if (!this.thinkingActive)
|
|
231
346
|
return;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
347
|
+
const text = this.thinkingText.trim() || "…";
|
|
348
|
+
this.addTimedResult(this.thinkingActive, text);
|
|
349
|
+
this.thinkingActive = null;
|
|
350
|
+
this.thinkingText = "";
|
|
351
|
+
}
|
|
352
|
+
/** Add a one-shot info entry (error, system, mode, config, model). */
|
|
353
|
+
addInfo(emoji, label, detail) {
|
|
354
|
+
this.cancelEmptyCardTimer();
|
|
355
|
+
this.entries.push({ id: nextId(), kind: "info", emoji, label, detail });
|
|
356
|
+
this.requestFlush();
|
|
357
|
+
}
|
|
358
|
+
/** Add or replace the plan entry. */
|
|
359
|
+
setPlan(entries) {
|
|
360
|
+
this.cancelEmptyCardTimer();
|
|
361
|
+
if (this.planId) {
|
|
362
|
+
const entry = this.findEntry(this.planId);
|
|
363
|
+
if (entry && entry.kind === "plan") {
|
|
364
|
+
entry.entries = entries;
|
|
237
365
|
this.requestFlush();
|
|
238
|
-
// Check for overflow (root text limit)
|
|
239
|
-
if (this.ref)
|
|
240
|
-
this.checkSplit();
|
|
241
366
|
return;
|
|
242
367
|
}
|
|
243
368
|
}
|
|
244
|
-
|
|
369
|
+
this.planId = nextId();
|
|
370
|
+
this.entries.push({ id: this.planId, kind: "plan", entries });
|
|
371
|
+
this.requestFlush();
|
|
372
|
+
}
|
|
373
|
+
/** Add text — always appended at root level. */
|
|
374
|
+
addText(text) {
|
|
375
|
+
if (!text)
|
|
376
|
+
return;
|
|
377
|
+
this.cancelEmptyCardTimer();
|
|
245
378
|
const lastText = this.entries.filter((e) => e.kind === "text").at(-1);
|
|
246
|
-
if (lastText) {
|
|
379
|
+
if (lastText && lastText.kind === "text") {
|
|
247
380
|
lastText.text += text;
|
|
248
381
|
}
|
|
249
382
|
else {
|
|
@@ -251,29 +384,18 @@ export class SessionMessage {
|
|
|
251
384
|
}
|
|
252
385
|
this.resetStallTimer();
|
|
253
386
|
this.requestFlush();
|
|
254
|
-
// Check for overflow after root text grows
|
|
255
387
|
if (this.ref)
|
|
256
388
|
this.checkSplit();
|
|
257
389
|
}
|
|
258
|
-
/**
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
*/
|
|
263
|
-
addThought(text) {
|
|
264
|
-
const thoughts = this.entries.filter((e) => e.kind === "thought");
|
|
265
|
-
const last = thoughts[thoughts.length - 1];
|
|
266
|
-
if (last) {
|
|
267
|
-
// Add a space between consecutive chunks so they don't run together
|
|
268
|
-
last.text += ` ${text}`;
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
this.entries.push({ id: nextId(), kind: "thought", text });
|
|
272
|
-
}
|
|
390
|
+
/** Add an inline resource line (📎 prefix). */
|
|
391
|
+
addResource(text) {
|
|
392
|
+
this.cancelEmptyCardTimer();
|
|
393
|
+
this.entries.push({ id: nextId(), kind: "resource", text });
|
|
273
394
|
this.requestFlush();
|
|
274
395
|
}
|
|
275
|
-
/** Set or replace the usage
|
|
396
|
+
/** Set or replace the usage footer. Stops the working animation. */
|
|
276
397
|
setUsage(text) {
|
|
398
|
+
this.working = false;
|
|
277
399
|
if (this.usageId) {
|
|
278
400
|
const entry = this.findEntry(this.usageId);
|
|
279
401
|
if (entry && entry.kind === "usage")
|
|
@@ -283,6 +405,9 @@ export class SessionMessage {
|
|
|
283
405
|
this.usageId = nextId();
|
|
284
406
|
this.entries.push({ id: this.usageId, kind: "usage", text });
|
|
285
407
|
}
|
|
408
|
+
const hasRunning = this.entries.some((e) => e.kind === "timed" && !e.result);
|
|
409
|
+
if (!hasRunning)
|
|
410
|
+
this.stopTickInterval();
|
|
286
411
|
this.requestFlush();
|
|
287
412
|
}
|
|
288
413
|
/** Add a divider entry. */
|
|
@@ -290,29 +415,24 @@ export class SessionMessage {
|
|
|
290
415
|
this.entries.push({ id: nextId(), kind: "divider" });
|
|
291
416
|
this.requestFlush();
|
|
292
417
|
}
|
|
293
|
-
// ─── Entry helpers
|
|
418
|
+
// ─── Entry helpers ──────────────────────────────────────────────────────
|
|
294
419
|
findEntry(id) {
|
|
295
|
-
|
|
296
|
-
if (entry.id === id)
|
|
297
|
-
return entry;
|
|
298
|
-
}
|
|
299
|
-
return undefined;
|
|
300
|
-
}
|
|
301
|
-
removeEntry(id) {
|
|
302
|
-
this.entries = this.entries.filter((e) => e.id !== id);
|
|
420
|
+
return this.entries.find((e) => e.id === id);
|
|
303
421
|
}
|
|
304
|
-
// ─── Periodic tick for elapsed time updates
|
|
305
|
-
/**
|
|
306
|
-
* Start a 1-second interval that updates elapsed time on running tool-progress
|
|
307
|
-
* entries. Called when a tool starts, stopped when tool completes or finalize.
|
|
308
|
-
*/
|
|
422
|
+
// ─── Periodic tick for elapsed time updates ─────────────────────────────
|
|
309
423
|
startTickInterval() {
|
|
310
424
|
if (this.tickInterval)
|
|
311
425
|
return;
|
|
312
426
|
this.tickInterval = setInterval(() => {
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
427
|
+
// Advance working dots animation
|
|
428
|
+
if (this.working && this.usageId) {
|
|
429
|
+
this.workingFrame = (this.workingFrame + 1) % WORKING_FRAMES.length;
|
|
430
|
+
const entry = this.findEntry(this.usageId);
|
|
431
|
+
if (entry && entry.kind === "usage")
|
|
432
|
+
entry.text = WORKING_FRAMES[this.workingFrame];
|
|
433
|
+
}
|
|
434
|
+
const hasRunning = this.entries.some((e) => e.kind === "timed" && !e.result);
|
|
435
|
+
if (!hasRunning && !this.working) {
|
|
316
436
|
this.stopTickInterval();
|
|
317
437
|
return;
|
|
318
438
|
}
|
|
@@ -327,27 +447,23 @@ export class SessionMessage {
|
|
|
327
447
|
this.tickInterval = undefined;
|
|
328
448
|
}
|
|
329
449
|
}
|
|
330
|
-
// ─── Card building
|
|
450
|
+
// ─── Card building ──────────────────────────────────────────────────────
|
|
331
451
|
buildCard() {
|
|
332
452
|
const body = buildCardBody(this.entries);
|
|
333
453
|
return {
|
|
334
454
|
type: "AdaptiveCard",
|
|
335
455
|
version: "1.4",
|
|
336
|
-
body: [
|
|
337
|
-
...(body.length > 0 ? body : [{ type: "TextBlock", text: "…" }]),
|
|
338
|
-
],
|
|
339
|
-
// Use full available width
|
|
456
|
+
body: body.length > 0 ? body : [{ type: "TextBlock", text: "…" }],
|
|
340
457
|
width: "stretch",
|
|
341
458
|
};
|
|
342
459
|
}
|
|
343
|
-
// ─── Flush / Rate limiting
|
|
460
|
+
// ─── Flush / Rate limiting ──────────────────────────────────────────────
|
|
344
461
|
flushTimer;
|
|
345
462
|
requestFlush() {
|
|
346
463
|
if (this.flushTimer)
|
|
347
464
|
clearTimeout(this.flushTimer);
|
|
348
465
|
this.flushTimer = setTimeout(() => {
|
|
349
466
|
this.flushTimer = undefined;
|
|
350
|
-
const card = this.buildCard();
|
|
351
467
|
const key = this.ref ? `update:${this.ref.activityId}` : `new:${this.sessionId}`;
|
|
352
468
|
this.rateLimiter.enqueue(this.conversationId, () => this.flush(), key).catch((err) => {
|
|
353
469
|
log.warn({ err, sessionId: this.sessionId }, "[SessionMessage] flush failed");
|
|
@@ -355,6 +471,8 @@ export class SessionMessage {
|
|
|
355
471
|
}, 500);
|
|
356
472
|
}
|
|
357
473
|
async flush() {
|
|
474
|
+
if (this.destroyed)
|
|
475
|
+
return;
|
|
358
476
|
const card = this.buildCard();
|
|
359
477
|
const cardStr = JSON.stringify(card);
|
|
360
478
|
if (!cardStr || cardStr === this.lastSent)
|
|
@@ -372,9 +490,8 @@ export class SessionMessage {
|
|
|
372
490
|
}
|
|
373
491
|
else {
|
|
374
492
|
const success = await this.updateCardViaRest(card);
|
|
375
|
-
if (success)
|
|
493
|
+
if (success)
|
|
376
494
|
this.lastSent = cardStr;
|
|
377
|
-
}
|
|
378
495
|
}
|
|
379
496
|
// Content arrived while flushing — request another flush
|
|
380
497
|
const current = this.buildCard();
|
|
@@ -392,10 +509,7 @@ export class SessionMessage {
|
|
|
392
509
|
try {
|
|
393
510
|
const response = await fetch(url, {
|
|
394
511
|
method: "PUT",
|
|
395
|
-
headers: {
|
|
396
|
-
"Content-Type": "application/json",
|
|
397
|
-
"Authorization": `Bearer ${token}`,
|
|
398
|
-
},
|
|
512
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
|
|
399
513
|
body: JSON.stringify({
|
|
400
514
|
type: "message",
|
|
401
515
|
attachments: [CardFactory.adaptiveCard(card)],
|
|
@@ -412,13 +526,12 @@ export class SessionMessage {
|
|
|
412
526
|
return false;
|
|
413
527
|
}
|
|
414
528
|
}
|
|
415
|
-
// ─── Stall timer
|
|
529
|
+
// ─── Stall timer ────────────────────────────────────────────────────────
|
|
416
530
|
resetStallTimer() {
|
|
417
531
|
if (this.stallTimer)
|
|
418
532
|
clearTimeout(this.stallTimer);
|
|
419
533
|
this.stallTimer = setTimeout(() => {
|
|
420
|
-
const hasContent = this.entries.some((e) =>
|
|
421
|
-
(e.kind === "tool" && e.children.length > 0));
|
|
534
|
+
const hasContent = this.entries.some((e) => e.kind === "text" && e.text.length > 0);
|
|
422
535
|
const hasUsage = this.entries.some((e) => e.kind === "usage");
|
|
423
536
|
if (hasContent && !hasUsage) {
|
|
424
537
|
log.warn({ sessionId: this.sessionId }, "[SessionMessage] Stream stalled — adding cutoff notice");
|
|
@@ -434,9 +547,16 @@ export class SessionMessage {
|
|
|
434
547
|
if (this.stallTimer.unref)
|
|
435
548
|
this.stallTimer.unref();
|
|
436
549
|
}
|
|
437
|
-
// ─── Finalize
|
|
550
|
+
// ─── Finalize ───────────────────────────────────────────────────────────
|
|
438
551
|
async finalize() {
|
|
552
|
+
this.cancelEmptyCardTimer();
|
|
553
|
+
this.closeActiveThinking();
|
|
554
|
+
this.working = false;
|
|
439
555
|
this.stopTickInterval();
|
|
556
|
+
if (this.flushTimer) {
|
|
557
|
+
clearTimeout(this.flushTimer);
|
|
558
|
+
this.flushTimer = undefined;
|
|
559
|
+
}
|
|
440
560
|
if (this.stallTimer) {
|
|
441
561
|
clearTimeout(this.stallTimer);
|
|
442
562
|
this.stallTimer = undefined;
|
|
@@ -461,7 +581,7 @@ export class SessionMessage {
|
|
|
461
581
|
}
|
|
462
582
|
return this.ref;
|
|
463
583
|
}
|
|
464
|
-
/**
|
|
584
|
+
/** Strip a pattern from root text entries. */
|
|
465
585
|
async stripPattern(pattern) {
|
|
466
586
|
for (const entry of this.entries) {
|
|
467
587
|
if (entry.kind === "text") {
|
|
@@ -472,10 +592,7 @@ export class SessionMessage {
|
|
|
472
592
|
}
|
|
473
593
|
}
|
|
474
594
|
}
|
|
475
|
-
|
|
476
|
-
* Check if root text exceeds limit and split into a new message.
|
|
477
|
-
* Called from addText when a message is already sent (this.ref != null).
|
|
478
|
-
*/
|
|
595
|
+
// ─── Text overflow / split ──────────────────────────────────────────────
|
|
479
596
|
checkSplit() {
|
|
480
597
|
const rootText = this.entries
|
|
481
598
|
.filter((e) => e.kind === "text")
|
|
@@ -485,7 +602,6 @@ export class SessionMessage {
|
|
|
485
602
|
return;
|
|
486
603
|
this.split();
|
|
487
604
|
}
|
|
488
|
-
/** For split: finalize current message at limit, start fresh. */
|
|
489
605
|
split() {
|
|
490
606
|
const rootText = this.entries
|
|
491
607
|
.filter((e) => e.kind === "text")
|
|
@@ -494,73 +610,51 @@ export class SessionMessage {
|
|
|
494
610
|
if (rootText.length <= MAX_ROOT_TEXT_LENGTH)
|
|
495
611
|
return;
|
|
496
612
|
let accLen = 0;
|
|
497
|
-
const
|
|
498
|
-
const
|
|
613
|
+
const keep = [];
|
|
614
|
+
const overflow = [];
|
|
499
615
|
for (const entry of this.entries) {
|
|
500
616
|
if (entry.kind === "text") {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
accLen += text.length;
|
|
617
|
+
if (accLen + entry.text.length <= MAX_ROOT_TEXT_LENGTH) {
|
|
618
|
+
keep.push(entry);
|
|
619
|
+
accLen += entry.text.length;
|
|
505
620
|
}
|
|
506
621
|
else {
|
|
507
|
-
|
|
622
|
+
overflow.push(entry);
|
|
508
623
|
}
|
|
509
624
|
}
|
|
510
625
|
else {
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
log.info({ sessionId: this.sessionId, kept: entriesToKeep.length, overflow: entriesToOverflow.length }, "[SessionMessage] splitting");
|
|
515
|
-
this.entries = entriesToKeep;
|
|
516
|
-
if (entriesToOverflow.length > 0) {
|
|
517
|
-
const overflowText = entriesToOverflow
|
|
518
|
-
.filter((e) => e.kind === "text")
|
|
519
|
-
.map((e) => e.text)
|
|
520
|
-
.join("");
|
|
521
|
-
if (overflowText) {
|
|
522
|
-
this.entries.push({ id: nextId(), kind: "text", text: overflowText });
|
|
626
|
+
keep.push(entry);
|
|
523
627
|
}
|
|
524
628
|
}
|
|
629
|
+
log.info({ sessionId: this.sessionId, kept: keep.length, overflow: overflow.length }, "[SessionMessage] splitting");
|
|
630
|
+
const overflowText = overflow
|
|
631
|
+
.filter((e) => e.kind === "text")
|
|
632
|
+
.map((e) => e.text)
|
|
633
|
+
.join("");
|
|
634
|
+
// Update the old card with only the kept entries (no overflow)
|
|
635
|
+
this.entries = keep;
|
|
525
636
|
if (this.ref) {
|
|
526
637
|
const card = this.buildCard();
|
|
527
638
|
this.rateLimiter.enqueue(this.conversationId, () => this.updateCardViaRest(card).then(() => { }), `update:${this.ref.activityId}`).catch(() => { });
|
|
528
639
|
}
|
|
640
|
+
// Start a new card with only the overflow text — reset all entry ID references
|
|
641
|
+
// so subsequent calls create new entries in the new card
|
|
529
642
|
this.ref = null;
|
|
530
643
|
this.lastSent = "";
|
|
644
|
+
this.titleId = null;
|
|
645
|
+
this.usageId = null;
|
|
646
|
+
this.planId = null;
|
|
647
|
+
this.entries = overflowText
|
|
648
|
+
? [{ id: nextId(), kind: "text", text: overflowText }]
|
|
649
|
+
: [];
|
|
531
650
|
this.requestFlush();
|
|
532
651
|
}
|
|
533
|
-
// ─── Legacy API (no-ops for compat) ────────────────────────────────────────
|
|
534
|
-
/** @deprecated — use addThought instead */
|
|
535
|
-
setHeader(_text) { }
|
|
536
|
-
/** @deprecated — use addToolResult instead */
|
|
537
|
-
setHeaderResult(_text) { }
|
|
538
|
-
/** @deprecated — use addText instead */
|
|
539
|
-
appendBody(text) {
|
|
540
|
-
this.addText(text);
|
|
541
|
-
}
|
|
542
|
-
/** @deprecated — use setUsage instead */
|
|
543
|
-
setFooter(text) {
|
|
544
|
-
this.setUsage(text);
|
|
545
|
-
}
|
|
546
|
-
/** @deprecated — use setUsage instead */
|
|
547
|
-
appendFooter(text) {
|
|
548
|
-
const current = this.getFooter();
|
|
549
|
-
this.setUsage(current ? `${current} · ${text}` : text);
|
|
550
|
-
}
|
|
551
|
-
/** @deprecated — header zone is gone */
|
|
552
|
-
clearHeader() { }
|
|
553
|
-
/** @deprecated — thoughts persist, use addThought */
|
|
554
|
-
removeThought() { }
|
|
555
|
-
/** @deprecated — use addToolStart + addToolResult */
|
|
556
|
-
updateToolResult(_id, _result) { }
|
|
557
652
|
}
|
|
558
|
-
// ─── SessionMessageManager
|
|
653
|
+
// ─── SessionMessageManager ────────────────────────────────────────────────────
|
|
559
654
|
export class SessionMessageManager {
|
|
560
655
|
rateLimiter;
|
|
561
656
|
acquireBotToken;
|
|
562
657
|
messages = new Map();
|
|
563
|
-
planRefs = new Map();
|
|
564
658
|
constructor(rateLimiter, acquireBotToken) {
|
|
565
659
|
this.rateLimiter = rateLimiter;
|
|
566
660
|
this.acquireBotToken = acquireBotToken;
|
|
@@ -588,25 +682,16 @@ export class SessionMessageManager {
|
|
|
588
682
|
if (!msg)
|
|
589
683
|
return null;
|
|
590
684
|
this.messages.delete(sessionId);
|
|
591
|
-
this.planRefs.delete(sessionId);
|
|
592
685
|
return msg.finalize();
|
|
593
686
|
}
|
|
594
|
-
getPlanRef(sessionId) {
|
|
595
|
-
return this.planRefs.get(sessionId);
|
|
596
|
-
}
|
|
597
|
-
setPlanRef(sessionId, ref) {
|
|
598
|
-
this.planRefs.set(sessionId, ref);
|
|
599
|
-
}
|
|
600
687
|
cleanup(sessionId) {
|
|
601
688
|
const msg = this.messages.get(sessionId);
|
|
602
|
-
if (msg)
|
|
689
|
+
if (msg)
|
|
603
690
|
msg.finalize().catch(() => { });
|
|
604
|
-
}
|
|
605
691
|
this.messages.delete(sessionId);
|
|
606
|
-
this.planRefs.delete(sessionId);
|
|
607
692
|
}
|
|
608
693
|
}
|
|
609
|
-
// ─── Helpers
|
|
694
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
610
695
|
async function sendCard(context, card) {
|
|
611
696
|
const activity = {
|
|
612
697
|
type: "message",
|