pi-onlyne 2.1.0 → 2.3.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/README.md +77 -35
- package/README.zh.md +48 -19
- package/package.json +1 -1
- package/src/activity.mjs +294 -0
- package/src/activity.test.mjs +69 -1
- package/src/agent.mjs +157 -84
- package/src/agent.test.mjs +97 -45
- package/src/config.mjs +173 -3
- package/src/config.test.mjs +37 -1
- package/src/index.ts +163 -18
- package/src/pi-surface.mjs +69 -8
- package/src/pi-surface.test.mjs +78 -4
- package/src/session-scope.mjs +90 -0
- package/src/session-scope.test.mjs +61 -0
- package/src/socket.mjs +45 -0
- package/src/socket.test.mjs +27 -0
package/src/activity.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** The widget key shared with pi-surface. */
|
|
2
2
|
export const WIDGET_KEY = "onlyne";
|
|
3
|
+
/** The custom message type registered for visible inbound delivery cards. */
|
|
4
|
+
export const ASSIGN_MESSAGE_TYPE = "onlyne_assign";
|
|
3
5
|
/** Render budget below pi's own ten-line cap. */
|
|
4
6
|
export const MAX_LINES = 8;
|
|
5
7
|
/** Line budget that keeps the panel inside one terminal row. */
|
|
@@ -19,6 +21,21 @@ const MARKERS = {
|
|
|
19
21
|
state: "..",
|
|
20
22
|
};
|
|
21
23
|
|
|
24
|
+
const EVENT_STYLES = {
|
|
25
|
+
in: { fg: "accent" },
|
|
26
|
+
dup: { fg: "dim" },
|
|
27
|
+
out: { fg: "success" },
|
|
28
|
+
warn: { fg: "warning" },
|
|
29
|
+
state: { fg: "muted" },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const CONNECTION_STYLES = {
|
|
33
|
+
connected: { fg: "success", status: "done" },
|
|
34
|
+
reconnecting: { fg: "warning", status: "running" },
|
|
35
|
+
detached: { fg: "muted", status: "cancelled" },
|
|
36
|
+
connecting: { fg: "accent", status: "pending" },
|
|
37
|
+
};
|
|
38
|
+
|
|
22
39
|
const clean = (value) => String(value ?? "").replace(/\s+/g, " ").trim();
|
|
23
40
|
const points = (value) => Array.from(String(value ?? ""));
|
|
24
41
|
|
|
@@ -62,6 +79,237 @@ function eventLine(event) {
|
|
|
62
79
|
return cut(`${timeOf(event.at)} ${marker} ${event.text}${count}`, MAX_WIDTH);
|
|
63
80
|
}
|
|
64
81
|
|
|
82
|
+
function toneFg(theme, color, text) {
|
|
83
|
+
if (!theme || typeof theme.fg !== "function") return text;
|
|
84
|
+
try {
|
|
85
|
+
return theme.fg(color, text);
|
|
86
|
+
} catch {
|
|
87
|
+
return text;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toneBold(theme, text) {
|
|
92
|
+
if (!theme || typeof theme.bold !== "function") return text;
|
|
93
|
+
try {
|
|
94
|
+
return theme.bold(text);
|
|
95
|
+
} catch {
|
|
96
|
+
return text;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function styledHeaderLine(state, theme, width) {
|
|
101
|
+
if (!theme || typeof theme.fg !== "function") return headerLine(state);
|
|
102
|
+
const role = clean(state.role);
|
|
103
|
+
const connection = clean(state.connection) || "connecting";
|
|
104
|
+
const connStyle = CONNECTION_STYLES[connection] ?? CONNECTION_STYLES.connecting;
|
|
105
|
+
const sep = toneFg(theme, "dim", " · ");
|
|
106
|
+
const head = role
|
|
107
|
+
? `${toneBold(theme, toneFg(theme, "accent", "onlyne"))} ${toneBold(theme, cut(role, 16, false))}`
|
|
108
|
+
: toneBold(theme, toneFg(theme, "accent", "onlyne"));
|
|
109
|
+
const parts = [head, toneFg(theme, connStyle.fg, connection)];
|
|
110
|
+
if (state.generation !== undefined && state.generation !== null) {
|
|
111
|
+
parts.push(toneFg(theme, "muted", `gen ${state.generation}`));
|
|
112
|
+
}
|
|
113
|
+
const taskId = clean(state.taskId);
|
|
114
|
+
if (taskId) {
|
|
115
|
+
const phase = clean(state.phase);
|
|
116
|
+
const phaseTone = phase === "done"
|
|
117
|
+
? "success"
|
|
118
|
+
: phase === "failed" || phase === "blocked"
|
|
119
|
+
? "error"
|
|
120
|
+
: phase.includes("queued") || phase === "running"
|
|
121
|
+
? "warning"
|
|
122
|
+
: "muted";
|
|
123
|
+
const taskPart = `${toneFg(theme, "muted", "task")} ${toneFg(theme, "accent", cut(taskId, 8, false))}${
|
|
124
|
+
phase ? ` ${toneFg(theme, phaseTone, phase)}` : ""
|
|
125
|
+
}`;
|
|
126
|
+
parts.push(taskPart);
|
|
127
|
+
}
|
|
128
|
+
return parts.join(sep);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function styledEventLine(event, theme, maxTextWidth) {
|
|
132
|
+
if (!theme || typeof theme.fg !== "function") return eventLine(event);
|
|
133
|
+
const marker = MARKERS[event.kind] ?? MARKERS.state;
|
|
134
|
+
const style = EVENT_STYLES[event.kind] ?? EVENT_STYLES.state;
|
|
135
|
+
const count = event.repeats > 0 ? toneFg(theme, "warning", ` ×${event.repeats + 1}`) : "";
|
|
136
|
+
const bodyBudget = Math.max(12, maxTextWidth - 14 - (event.repeats > 0 ? String(event.repeats + 1).length + 2 : 0));
|
|
137
|
+
const body = cut(event.text, bodyBudget);
|
|
138
|
+
const bodyStyled = event.kind === "warn"
|
|
139
|
+
? toneFg(theme, "warning", body)
|
|
140
|
+
: event.kind === "dup"
|
|
141
|
+
? toneFg(theme, "dim", body)
|
|
142
|
+
: body;
|
|
143
|
+
return `${toneFg(theme, "dim", timeOf(event.at))} ${toneFg(theme, style.fg, marker)} ${bodyStyled}${count}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function describeHeaderSpans(state) {
|
|
147
|
+
const role = clean(state.role);
|
|
148
|
+
const connection = clean(state.connection) || "connecting";
|
|
149
|
+
const connStyle = CONNECTION_STYLES[connection] ?? CONNECTION_STYLES.connecting;
|
|
150
|
+
const spans = [{ t: "onlyne", s: "accent strong" }];
|
|
151
|
+
if (role) spans.push({ t: ` ${cut(role, 16, false)}`, s: "strong" });
|
|
152
|
+
spans.push({ t: " · ", s: "dim" }, { t: connection, s: connStyle.fg });
|
|
153
|
+
if (state.generation !== undefined && state.generation !== null) {
|
|
154
|
+
spans.push({ t: " · ", s: "dim" }, { t: `gen ${state.generation}`, s: "muted" });
|
|
155
|
+
}
|
|
156
|
+
const taskId = clean(state.taskId);
|
|
157
|
+
if (taskId) {
|
|
158
|
+
const phase = clean(state.phase);
|
|
159
|
+
const phaseTone = phase === "done"
|
|
160
|
+
? "success"
|
|
161
|
+
: phase === "failed" || phase === "blocked"
|
|
162
|
+
? "error"
|
|
163
|
+
: phase.includes("queued") || phase === "running"
|
|
164
|
+
? "warning"
|
|
165
|
+
: "muted";
|
|
166
|
+
spans.push(
|
|
167
|
+
{ t: " · ", s: "dim" },
|
|
168
|
+
{ t: "task ", s: "muted" },
|
|
169
|
+
{ t: cut(taskId, 8, false), s: "accent mono" },
|
|
170
|
+
);
|
|
171
|
+
if (phase) spans.push({ t: ` ${phase}`, s: phaseTone });
|
|
172
|
+
}
|
|
173
|
+
return spans;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function describeEventRow(event, index) {
|
|
177
|
+
const marker = MARKERS[event.kind] ?? MARKERS.state;
|
|
178
|
+
const style = EVENT_STYLES[event.kind] ?? EVENT_STYLES.state;
|
|
179
|
+
const bodyTone = event.kind === "warn" ? "warning" : event.kind === "dup" ? "dim" : undefined;
|
|
180
|
+
const spans = [
|
|
181
|
+
{ t: timeOf(event.at), s: "dim mono" },
|
|
182
|
+
{ t: ` ${marker} `, s: `${style.fg} mono strong` },
|
|
183
|
+
bodyTone ? { t: event.text, s: bodyTone } : { t: event.text },
|
|
184
|
+
];
|
|
185
|
+
if (event.repeats > 0) {
|
|
186
|
+
spans.push({ t: ` ×${event.repeats + 1}`, s: "warning mono" });
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
k: "text",
|
|
190
|
+
p: { spans, truncate: "end", role: `onlyne.event.${event.kind}` },
|
|
191
|
+
key: `ev-${index}`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Build a TSP `NativeNode` card for Tern surfaces (`Component.describe()`).
|
|
197
|
+
*/
|
|
198
|
+
export function describeActivityNode(state, events, shown = SHOW_EVENTS, collapsed = false) {
|
|
199
|
+
const connection = clean(state.connection) || "connecting";
|
|
200
|
+
const connStyle = CONNECTION_STYLES[connection] ?? CONNECTION_STYLES.connecting;
|
|
201
|
+
const visible = events.slice(0, shown);
|
|
202
|
+
return {
|
|
203
|
+
k: "card",
|
|
204
|
+
p: {
|
|
205
|
+
role: "omp.widget.onlyne",
|
|
206
|
+
variant: "bare",
|
|
207
|
+
status: connStyle.status,
|
|
208
|
+
head: describeHeaderSpans(state),
|
|
209
|
+
collapsible: visible.length > 0,
|
|
210
|
+
collapsed: visible.length > 0 ? Boolean(collapsed) : false,
|
|
211
|
+
},
|
|
212
|
+
c: visible.map((ev, index) => describeEventRow(ev, index)),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Render an inbound Onlyne delivery (`customType: "onlyne_assign"`) as a
|
|
218
|
+
* framed transcript block on ANSI terminals and a native TSP card on Tern.
|
|
219
|
+
*/
|
|
220
|
+
export function createAssignMessageComponent(details, messageContent, theme, collapsed = false) {
|
|
221
|
+
const from = clean(details?.from) || "cluster";
|
|
222
|
+
const kind = clean(details?.kind) || "task";
|
|
223
|
+
const taskId = clean(details?.taskId);
|
|
224
|
+
const shortId = taskId ? cut(taskId, 8, false) : "";
|
|
225
|
+
const rawBody = clean(details?.bodyText)
|
|
226
|
+
|| (typeof messageContent === "string"
|
|
227
|
+
? messageContent
|
|
228
|
+
: Array.isArray(messageContent)
|
|
229
|
+
? messageContent.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n")
|
|
230
|
+
: "");
|
|
231
|
+
const bodyLines = String(details?.bodyText ?? (typeof messageContent === "string" ? messageContent : rawBody))
|
|
232
|
+
.replace(/\r\n/g, "\n")
|
|
233
|
+
.split("\n");
|
|
234
|
+
const headLabel = `From ${from} · ${kind}${shortId ? ` · ${shortId}` : ""}`;
|
|
235
|
+
const kindTone = kind === "note" ? "info" : kind === "nudge" ? "warning" : "accent";
|
|
236
|
+
let cachedWidth = -1;
|
|
237
|
+
let cachedRows = null;
|
|
238
|
+
let cachedNode = null;
|
|
239
|
+
let isCollapsed = Boolean(collapsed);
|
|
240
|
+
|
|
241
|
+
const buildNode = () => {
|
|
242
|
+
const headSpans = [
|
|
243
|
+
{ t: `From ${from}`, s: "accent strong" },
|
|
244
|
+
{ t: " · ", s: "dim" },
|
|
245
|
+
{ t: kind, s: `${kindTone} mono` },
|
|
246
|
+
];
|
|
247
|
+
if (shortId) {
|
|
248
|
+
headSpans.push({ t: " · ", s: "dim" }, { t: shortId, s: "muted mono" });
|
|
249
|
+
}
|
|
250
|
+
const mdText = String(details?.bodyText ?? (typeof messageContent === "string" ? messageContent : rawBody)).trim();
|
|
251
|
+
return {
|
|
252
|
+
k: "card",
|
|
253
|
+
p: {
|
|
254
|
+
role: `omp.custom.onlyne.${kind}`,
|
|
255
|
+
tone: kind === "nudge" ? "warning" : "accent",
|
|
256
|
+
head: headSpans,
|
|
257
|
+
collapsible: true,
|
|
258
|
+
collapsed: isCollapsed,
|
|
259
|
+
},
|
|
260
|
+
c: mdText ? [{ k: "md", p: { text: mdText }, key: "body" }] : [],
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const buildRows = (width = MAX_WIDTH) => {
|
|
265
|
+
const w = Math.max(16, Math.min(width, MAX_WIDTH));
|
|
266
|
+
const inner = Math.max(12, w - 4);
|
|
267
|
+
const topText = ` ${cut(headLabel, inner - 2)} `;
|
|
268
|
+
const topFill = "─".repeat(Math.max(0, w - 2 - points(topText).length));
|
|
269
|
+
const rows = [
|
|
270
|
+
`${toneFg(theme, "muted", "╭")}${toneBold(theme, toneFg(theme, "accent", topText))}${toneFg(theme, "muted", `${topFill}╮`)}`,
|
|
271
|
+
];
|
|
272
|
+
const visibleBody = isCollapsed
|
|
273
|
+
? [cut(clean(rawBody), inner)]
|
|
274
|
+
: bodyLines.flatMap((line) => {
|
|
275
|
+
const trimmed = line.replace(/\t/g, " ");
|
|
276
|
+
return trimmed.length === 0 ? [""] : [cut(trimmed, inner)];
|
|
277
|
+
});
|
|
278
|
+
for (const line of visibleBody) {
|
|
279
|
+
const pad = " ".repeat(Math.max(0, inner - points(line).length));
|
|
280
|
+
rows.push(`${toneFg(theme, "muted", "│")} ${toneFg(theme, "text", line)}${pad} ${toneFg(theme, "muted", "│")}`);
|
|
281
|
+
}
|
|
282
|
+
rows.push(`${toneFg(theme, "muted", `╰${"─".repeat(Math.max(0, w - 2))}╯`)}`);
|
|
283
|
+
return rows;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
render(width = MAX_WIDTH) {
|
|
288
|
+
if (cachedRows && cachedWidth === width) return cachedRows;
|
|
289
|
+
cachedWidth = width;
|
|
290
|
+
cachedRows = buildRows(width);
|
|
291
|
+
return cachedRows;
|
|
292
|
+
},
|
|
293
|
+
describe() {
|
|
294
|
+
cachedNode ??= buildNode();
|
|
295
|
+
return cachedNode;
|
|
296
|
+
},
|
|
297
|
+
handleNativeEvent(event) {
|
|
298
|
+
if (event?.type === "toggle" && typeof event.collapsed === "boolean") {
|
|
299
|
+
isCollapsed = event.collapsed;
|
|
300
|
+
cachedRows = null;
|
|
301
|
+
cachedNode = null;
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
invalidate() {
|
|
305
|
+
cachedWidth = -1;
|
|
306
|
+
cachedRows = null;
|
|
307
|
+
cachedNode = null;
|
|
308
|
+
},
|
|
309
|
+
dispose() {},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
65
313
|
/**
|
|
66
314
|
* Activity is a pure panel model: header state plus newest-first event history.
|
|
67
315
|
* `lines()` reads stored state only, so identical state gives identical output.
|
|
@@ -70,6 +318,7 @@ export function createActivity({ maxEvents = SHOW_EVENTS, clock = () => new Date
|
|
|
70
318
|
const state = { connection: "connecting" };
|
|
71
319
|
const events = [];
|
|
72
320
|
const shown = Math.max(0, Math.min(Number(maxEvents) || 0, MAX_LINES - 1));
|
|
321
|
+
let collapsed = false;
|
|
73
322
|
const api = {
|
|
74
323
|
note(kind, text) {
|
|
75
324
|
const next = noteText(text);
|
|
@@ -90,6 +339,51 @@ export function createActivity({ maxEvents = SHOW_EVENTS, clock = () => new Date
|
|
|
90
339
|
lines() {
|
|
91
340
|
return [headerLine(state), ...events.slice(0, shown).map(eventLine)].slice(0, MAX_LINES);
|
|
92
341
|
},
|
|
342
|
+
ansiLines(theme, width = MAX_WIDTH) {
|
|
343
|
+
const padX = width >= 4 ? " " : "";
|
|
344
|
+
const innerWidth = Math.max(24, Math.min(MAX_WIDTH, width - padX.length * 2));
|
|
345
|
+
if (collapsed) {
|
|
346
|
+
return [`${padX}${styledHeaderLine(state, theme, innerWidth)}`];
|
|
347
|
+
}
|
|
348
|
+
return [
|
|
349
|
+
`${padX}${styledHeaderLine(state, theme, innerWidth)}`,
|
|
350
|
+
...events.slice(0, shown).map((ev) => `${padX}${styledEventLine(ev, theme, innerWidth)}`),
|
|
351
|
+
].slice(0, MAX_LINES);
|
|
352
|
+
},
|
|
353
|
+
describe() {
|
|
354
|
+
return describeActivityNode(state, events, shown, collapsed);
|
|
355
|
+
},
|
|
356
|
+
component(theme, tui = null) {
|
|
357
|
+
let cachedWidth = -1;
|
|
358
|
+
let cachedRows = null;
|
|
359
|
+
let cachedNode = null;
|
|
360
|
+
return {
|
|
361
|
+
render(width = MAX_WIDTH) {
|
|
362
|
+
if (cachedRows && cachedWidth === width) return cachedRows;
|
|
363
|
+
cachedWidth = width;
|
|
364
|
+
cachedRows = api.ansiLines(theme, width);
|
|
365
|
+
return cachedRows;
|
|
366
|
+
},
|
|
367
|
+
describe() {
|
|
368
|
+
cachedNode ??= api.describe();
|
|
369
|
+
return cachedNode;
|
|
370
|
+
},
|
|
371
|
+
handleNativeEvent(event) {
|
|
372
|
+
if (event?.type === "toggle" && typeof event.collapsed === "boolean") {
|
|
373
|
+
collapsed = event.collapsed;
|
|
374
|
+
cachedRows = null;
|
|
375
|
+
cachedNode = null;
|
|
376
|
+
tui?.requestRender?.();
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
invalidate() {
|
|
380
|
+
cachedWidth = -1;
|
|
381
|
+
cachedRows = null;
|
|
382
|
+
cachedNode = null;
|
|
383
|
+
},
|
|
384
|
+
dispose() {},
|
|
385
|
+
};
|
|
386
|
+
},
|
|
93
387
|
events,
|
|
94
388
|
};
|
|
95
389
|
return api;
|
package/src/activity.test.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
|
|
4
|
-
import { createActivity, MAX_LINES, MAX_TEXT, MAX_WIDTH, SHOW_EVENTS } from "./activity.mjs";
|
|
4
|
+
import { createActivity, createAssignMessageComponent, MAX_LINES, MAX_TEXT, MAX_WIDTH, SHOW_EVENTS } from "./activity.mjs";
|
|
5
5
|
|
|
6
6
|
const clock = () => new Date("2026-09-13T09:08:07");
|
|
7
7
|
|
|
@@ -61,3 +61,71 @@ test("default render shows the newest SHOW_EVENTS events", () => {
|
|
|
61
61
|
assert.match(lines[1], /event 9$/);
|
|
62
62
|
assert.match(lines.at(-1), new RegExp(`event ${SHOW_EVENTS + 4 - SHOW_EVENTS}$`));
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
test("describe() produces a Tern TSP card node and component() supports toggle", () => {
|
|
66
|
+
const activity = createActivity({ clock }).set({
|
|
67
|
+
role: "dpdp",
|
|
68
|
+
connection: "connected",
|
|
69
|
+
generation: 1,
|
|
70
|
+
taskId: "abcdef012345",
|
|
71
|
+
phase: "running",
|
|
72
|
+
});
|
|
73
|
+
activity.note("state", "connected role=dpdp gen=1 host=probe,recycle");
|
|
74
|
+
activity.note("warn", "heartbeat refused").note("warn", "heartbeat refused");
|
|
75
|
+
|
|
76
|
+
const node = activity.describe();
|
|
77
|
+
assert.equal(node.k, "card");
|
|
78
|
+
assert.equal(node.p.role, "omp.widget.onlyne");
|
|
79
|
+
assert.equal(node.p.variant, "bare");
|
|
80
|
+
assert.equal(node.p.status, "done");
|
|
81
|
+
assert.equal(node.p.collapsible, true);
|
|
82
|
+
assert.equal(node.p.collapsed, false);
|
|
83
|
+
assert.equal(node.c.length, 2);
|
|
84
|
+
assert.equal(node.c[0].k, "text");
|
|
85
|
+
assert.equal(node.c[0].p.role, "onlyne.event.warn");
|
|
86
|
+
assert.ok(node.c[0].p.spans.some((s) => s.t === " ×2"));
|
|
87
|
+
|
|
88
|
+
const theme = {
|
|
89
|
+
fg: (color, text) => `<${color}>${text}</${color}>`,
|
|
90
|
+
bold: (text) => `<b>${text}</b>`,
|
|
91
|
+
};
|
|
92
|
+
let rendered = 0;
|
|
93
|
+
const comp = activity.component(theme, { requestRender: () => { rendered += 1; } });
|
|
94
|
+
const rows = comp.render(80);
|
|
95
|
+
assert.equal(rows.length, 3);
|
|
96
|
+
assert.ok(rows[0].startsWith(" <b><accent>onlyne</accent></b> <b>dpdp</b>"));
|
|
97
|
+
assert.equal(comp.render(80), rows, "same width returns cached rows reference");
|
|
98
|
+
|
|
99
|
+
comp.handleNativeEvent({ type: "toggle", collapsed: true });
|
|
100
|
+
assert.equal(rendered, 1);
|
|
101
|
+
assert.equal(comp.describe().p.collapsed, true);
|
|
102
|
+
assert.equal(comp.render(80).length, 1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("createAssignMessageComponent renders ANSI box and TSP card for onlyne_assign", () => {
|
|
106
|
+
const theme = {
|
|
107
|
+
fg: (color, text) => `<${color}>${text}</${color}>`,
|
|
108
|
+
bold: (text) => `<b>${text}</b>`,
|
|
109
|
+
};
|
|
110
|
+
const comp = createAssignMessageComponent(
|
|
111
|
+
{
|
|
112
|
+
role: "caerulea",
|
|
113
|
+
taskId: "97971b93-1111-2222-3333-444444444444",
|
|
114
|
+
kind: "note",
|
|
115
|
+
from: "role:_supervisor",
|
|
116
|
+
bodyText: "hello caerulea",
|
|
117
|
+
},
|
|
118
|
+
"From _supervisor:\n\nhello caerulea",
|
|
119
|
+
theme,
|
|
120
|
+
false,
|
|
121
|
+
);
|
|
122
|
+
const rows = comp.render(72);
|
|
123
|
+
assert.ok(rows[0].includes("From role:_supervisor · note · 97971b93"));
|
|
124
|
+
assert.ok(rows.some((r) => r.includes("hello caerulea")));
|
|
125
|
+
|
|
126
|
+
const node = comp.describe();
|
|
127
|
+
assert.equal(node.k, "card");
|
|
128
|
+
assert.equal(node.p.role, "omp.custom.onlyne.note");
|
|
129
|
+
assert.equal(node.c[0].k, "md");
|
|
130
|
+
assert.equal(node.c[0].p.text, "hello caerulea");
|
|
131
|
+
});
|