@fusion.dev/helix-headless 0.212.1 → 0.213.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/api.json +1 -1
- package/custom-elements.json +1 -1
- package/dist/chat/root/chat-root.d.ts +38 -2
- package/dist/chat/root/chat-root.js +42 -3
- package/dist/log/buffer.d.ts +35 -2
- package/dist/log/buffer.js +39 -2
- package/dist/log/index.d.ts +1 -1
- package/dist/log/index.js +1 -0
- package/dist/log/line/log-line.d.ts +34 -3
- package/dist/log/line/log-line.js +41 -5
- package/dist/log/root/log-root.d.ts +27 -3
- package/dist/log/root/log-root.js +24 -8
- package/dist/log/rows.d.ts +4 -2
- package/dist/log/rows.js +11 -7
- package/dist/terminal/keys.d.ts +1 -1
- package/dist/terminal/keys.js +8 -3
- package/dist/terminal/root/terminal-root.d.ts +51 -1
- package/dist/terminal/root/terminal-root.js +101 -3
- package/examples.json +1 -1
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +242 -10
- package/types/vue.d.ts +62 -2
- package/types/words.d.ts +41 -0
|
@@ -346,6 +346,8 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
|
|
|
346
346
|
private grower;
|
|
347
347
|
/** What `grower` is watching, so a render that changed nothing re-observes nothing. */
|
|
348
348
|
private watched;
|
|
349
|
+
/** The frame `watchSizes` will observe in, while one is booked. */
|
|
350
|
+
private watchFrame;
|
|
349
351
|
/** The scroll offset this element last set, which the reader has not moved from. */
|
|
350
352
|
private stuckAt;
|
|
351
353
|
/** The turn at the top of the reader's view, and how far below the top it sat. */
|
|
@@ -424,7 +426,15 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
|
|
|
424
426
|
* a reader asking to go to the bottom means.
|
|
425
427
|
*/
|
|
426
428
|
resume(): void;
|
|
427
|
-
/**
|
|
429
|
+
/**
|
|
430
|
+
* Scrolls to the bottom, and remembers where that was.
|
|
431
|
+
*
|
|
432
|
+
* **At once, whatever `scroll-behavior` the page gives the log.** Pinned,
|
|
433
|
+
* this runs on every update, every token of a streaming turn included, and a
|
|
434
|
+
* smooth scroll sent again each time never arrives: in WebKit, a log that had
|
|
435
|
+
* streamed for a second under `scroll-behavior: smooth` rested 6 pixels short
|
|
436
|
+
* of the bottom, and `stuckAt` held where each scroll began.
|
|
437
|
+
*/
|
|
428
438
|
private stick;
|
|
429
439
|
/**
|
|
430
440
|
* Asks to pin or unpin, and reports whether the request stood.
|
|
@@ -496,8 +506,34 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
|
|
|
496
506
|
* in a conversation of several knows who spoke.
|
|
497
507
|
*/
|
|
498
508
|
private phrase;
|
|
499
|
-
/**
|
|
509
|
+
/**
|
|
510
|
+
* Watches what is drawn for size, re-observing only when that changed.
|
|
511
|
+
*
|
|
512
|
+
* **In the next frame's animation callbacks, never in the update that asked.**
|
|
513
|
+
* A resize notice asks for an update, the update draws new turns while the
|
|
514
|
+
* browser is still delivering notices, and turns observed then are held to
|
|
515
|
+
* the next frame with "ResizeObserver loop completed with undelivered
|
|
516
|
+
* notifications": twice in WebKit's run of Chat's specs and once in
|
|
517
|
+
* Chromium's, on October 11, 2026. Observed from the next frame's callbacks,
|
|
518
|
+
* their first notice comes before that frame paints, and a turn drawn for the
|
|
519
|
+
* first time is answered sooner still, by `settleDrawn`.
|
|
520
|
+
*/
|
|
500
521
|
private watchSizes;
|
|
522
|
+
/** Observes the log and the turns drawn now, where that changed. */
|
|
523
|
+
private observeSizes;
|
|
524
|
+
/**
|
|
525
|
+
* Answers the turns this render drew for the first time, once each has drawn
|
|
526
|
+
* what is in it: a microtask after this update, and before the frame.
|
|
527
|
+
*
|
|
528
|
+
* **Sooner than the resize observer, which `watchSizes` starts a frame
|
|
529
|
+
* later.** A turn is drawn empty and fills in after this update, and until
|
|
530
|
+
* it is measured the reader's turn sits where the estimates put it. A timer
|
|
531
|
+
* that ran before the next frame read that place as the reader's: on a Linux
|
|
532
|
+
* runner, the controller's rest after a scroll left a reader's turn 55.5
|
|
533
|
+
* pixels from where it was when history arrived above it, in WebKit, on
|
|
534
|
+
* October 11, 2026. Answered here, the history is held before any timer runs.
|
|
535
|
+
*/
|
|
536
|
+
private settleDrawn;
|
|
501
537
|
/**
|
|
502
538
|
* A turn or the log changed size, which moves nothing on its own.
|
|
503
539
|
*
|
|
@@ -62,6 +62,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
62
62
|
this.settling = undefined;
|
|
63
63
|
this.grower = undefined;
|
|
64
64
|
this.watched = [];
|
|
65
|
+
this.watchFrame = 0;
|
|
65
66
|
this.stuckAt = -1;
|
|
66
67
|
this.anchor = undefined;
|
|
67
68
|
this.asked = false;
|
|
@@ -259,7 +260,12 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
259
260
|
stick() {
|
|
260
261
|
const log = this.log;
|
|
261
262
|
if (log) {
|
|
262
|
-
log.
|
|
263
|
+
if (typeof log.scrollTo === 'function') {
|
|
264
|
+
log.scrollTo({ behavior: 'instant', top: log.scrollHeight });
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
log.scrollTop = log.scrollHeight;
|
|
268
|
+
}
|
|
263
269
|
this.stuckAt = log.scrollTop;
|
|
264
270
|
}
|
|
265
271
|
}
|
|
@@ -335,7 +341,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
335
341
|
}
|
|
336
342
|
const found = turns[low];
|
|
337
343
|
this.anchor = found
|
|
338
|
-
? { id: found.id, offset: found.element.getBoundingClientRect().top - top }
|
|
344
|
+
? { at: log.scrollTop, id: found.id, offset: found.element.getBoundingClientRect().top - top }
|
|
339
345
|
: undefined;
|
|
340
346
|
}
|
|
341
347
|
restore() {
|
|
@@ -344,6 +350,10 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
344
350
|
if (!log || !anchor || (this.fed && this.virtualize.scrolling())) {
|
|
345
351
|
return;
|
|
346
352
|
}
|
|
353
|
+
if (!this.fed && Math.abs(log.scrollTop - anchor.at) >= 1) {
|
|
354
|
+
this.remember();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
347
357
|
const found = this.rendered().find((turn) => turn.id === anchor.id);
|
|
348
358
|
if (!found) {
|
|
349
359
|
return;
|
|
@@ -418,9 +428,21 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
418
428
|
: formatWords(this, 'arrivalLabel', { author: turn.by, text: turn.text });
|
|
419
429
|
}
|
|
420
430
|
watchSizes() {
|
|
421
|
-
if (typeof ResizeObserver === 'undefined') {
|
|
431
|
+
if (typeof ResizeObserver === 'undefined' || this.watchFrame !== 0) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (typeof requestAnimationFrame !== 'function') {
|
|
435
|
+
this.observeSizes();
|
|
422
436
|
return;
|
|
423
437
|
}
|
|
438
|
+
this.watchFrame = requestAnimationFrame(() => {
|
|
439
|
+
this.watchFrame = 0;
|
|
440
|
+
if (this.isConnected) {
|
|
441
|
+
this.observeSizes();
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
observeSizes() {
|
|
424
446
|
const log = this.log;
|
|
425
447
|
const targets = [...(log ? [log] : []), ...this.rendered().map((turn) => turn.element)];
|
|
426
448
|
if (targets.length === this.watched.length &&
|
|
@@ -434,6 +456,18 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
434
456
|
}
|
|
435
457
|
this.watched = targets;
|
|
436
458
|
}
|
|
459
|
+
settleDrawn() {
|
|
460
|
+
const waiting = this.drawn().filter((row) => !drawnInFull(row));
|
|
461
|
+
if (waiting.length === 0) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
void Promise.all(waiting.map(({ element }) => element.firstElementChild
|
|
465
|
+
?.updateComplete)).then(() => {
|
|
466
|
+
if (this.isConnected && this.fed) {
|
|
467
|
+
this.handleResize();
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
437
471
|
connectedCallback() {
|
|
438
472
|
super.connectedCallback();
|
|
439
473
|
this.addEventListener('keydown', this.handleKeydown);
|
|
@@ -458,6 +492,10 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
458
492
|
this.settling = undefined;
|
|
459
493
|
this.grower?.disconnect();
|
|
460
494
|
this.watched = [];
|
|
495
|
+
if (this.watchFrame !== 0) {
|
|
496
|
+
cancelAnimationFrame(this.watchFrame);
|
|
497
|
+
this.watchFrame = 0;
|
|
498
|
+
}
|
|
461
499
|
}
|
|
462
500
|
willUpdate(changed) {
|
|
463
501
|
super.willUpdate(changed);
|
|
@@ -518,6 +556,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
518
556
|
}
|
|
519
557
|
if (this.fed) {
|
|
520
558
|
this.virtualize.measure();
|
|
559
|
+
this.settleDrawn();
|
|
521
560
|
}
|
|
522
561
|
this.watchSizes();
|
|
523
562
|
this.markScroller();
|
package/dist/log/buffer.d.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { type LogRun } from './ansi.js';
|
|
2
2
|
/** How much a line matters, which a stylesheet draws and a screen reader may hear. */
|
|
3
|
-
export type LogLevel = 'debug' | 'error' | 'info' | 'warn';
|
|
3
|
+
export type LogLevel = 'debug' | 'error' | 'info' | 'log' | 'warn';
|
|
4
|
+
/**
|
|
5
|
+
* The levels from the quietest to the loudest, which `level` on a log counts
|
|
6
|
+
* up from: `log` is a plain line, a console's `console.log`, below `info`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const LOG_LEVELS: readonly LogLevel[];
|
|
9
|
+
/** Whether a line at `level` shows under a log whose quietest level shown is `floor`. A line with no level is `info`. */
|
|
10
|
+
export declare const loudEnough: (level: LogLevel | undefined, floor: LogLevel | undefined) => boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The words a screen reader hears before a line's text, or nothing where the
|
|
13
|
+
* text already starts with them: `console.error(error)` writes `Error: …`,
|
|
14
|
+
* which `Error:` before it would read twice. The words' closing colon is not
|
|
15
|
+
* needed, and a word that only starts the text's first word, such as `Error`
|
|
16
|
+
* before `Errors`, is still said.
|
|
17
|
+
*/
|
|
18
|
+
export declare const levelSaid: (words: string | undefined, text: string) => string | undefined;
|
|
4
19
|
/** Where a step of the run stands. */
|
|
5
20
|
export type LogStatus = 'done' | 'failed' | 'pending' | 'running' | 'skipped';
|
|
6
21
|
/** One line of output, as `lines` holds it and `parse` returns it. */
|
|
@@ -9,6 +24,11 @@ export interface LogLine {
|
|
|
9
24
|
at?: number | string;
|
|
10
25
|
/** The `id` of the group in `groups` the line belongs to. */
|
|
11
26
|
group?: string;
|
|
27
|
+
/**
|
|
28
|
+
* How many times it was written in a row, where the log folds repeats with
|
|
29
|
+
* `collapse`. Absent is once.
|
|
30
|
+
*/
|
|
31
|
+
count?: number;
|
|
12
32
|
/** How much it matters. Absent is `info`. */
|
|
13
33
|
level?: LogLevel;
|
|
14
34
|
/** What was written, with any ANSI escape codes still in it. */
|
|
@@ -95,6 +115,8 @@ export declare class LogBuffer {
|
|
|
95
115
|
dropped: number;
|
|
96
116
|
/** The most finished lines kept, or 0 to keep them all. */
|
|
97
117
|
limit: number;
|
|
118
|
+
/** Whether a line written again straight after itself folds into it, with a count. */
|
|
119
|
+
collapse: boolean;
|
|
98
120
|
/** Reads each finished line of written text. */
|
|
99
121
|
parse: LogParser | undefined;
|
|
100
122
|
/** The steps `parse` opened, in order, replaced as a whole when one changes. */
|
|
@@ -119,8 +141,13 @@ export declare class LogBuffer {
|
|
|
119
141
|
* lines it finished.
|
|
120
142
|
*/
|
|
121
143
|
write(chunk: string): number;
|
|
122
|
-
/** Replaces every line, starting a fresh buffer that keeps its `parse` and `
|
|
144
|
+
/** Replaces every line, starting a fresh buffer that keeps its `parse`, `limit` and `collapse`. */
|
|
123
145
|
replace(lines: readonly LogLine[]): void;
|
|
146
|
+
/**
|
|
147
|
+
* Adds one finished line as data, after the lines held, folding it where
|
|
148
|
+
* `collapse` says, and lets the oldest go past `limit`.
|
|
149
|
+
*/
|
|
150
|
+
push(line: LogLine): void;
|
|
124
151
|
/** Lets every line go. */
|
|
125
152
|
clear(): void;
|
|
126
153
|
/** The line still being written, or undefined where the last line ended. */
|
|
@@ -142,6 +169,12 @@ export declare class LogBuffer {
|
|
|
142
169
|
* as a CI page folds a step that passed.
|
|
143
170
|
*/
|
|
144
171
|
private closeStep;
|
|
172
|
+
/**
|
|
173
|
+
* Keeps a finished line, or folds it into the line before it where it
|
|
174
|
+
* repeats that line and `collapse` is set. The fold is a new object, so a
|
|
175
|
+
* log comparing lines sees it change, drawn in the runs of the first.
|
|
176
|
+
*/
|
|
177
|
+
private add;
|
|
145
178
|
/** Lets the oldest lines go past `limit`, all at once. */
|
|
146
179
|
private trim;
|
|
147
180
|
}
|
package/dist/log/buffer.js
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { parseAnsi } from './ansi.js';
|
|
2
|
+
export const LOG_LEVELS = ['debug', 'log', 'info', 'warn', 'error'];
|
|
3
|
+
export const loudEnough = (level, floor) => floor === undefined || LOG_LEVELS.indexOf(level ?? 'info') >= LOG_LEVELS.indexOf(floor);
|
|
4
|
+
export const levelSaid = (words, text) => {
|
|
5
|
+
const word = words?.trim().replace(/:$/, '').toLocaleLowerCase();
|
|
6
|
+
if (!word) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const start = text.trimStart();
|
|
10
|
+
const repeats = start.slice(0, word.length).toLocaleLowerCase() === word &&
|
|
11
|
+
!/^[\p{L}\p{N}]/u.test(start.slice(word.length));
|
|
12
|
+
return repeats ? undefined : words;
|
|
13
|
+
};
|
|
14
|
+
const repeats = (line, last) => last !== undefined &&
|
|
15
|
+
last.text === line.text &&
|
|
16
|
+
last.level === line.level &&
|
|
17
|
+
last.group === line.group;
|
|
2
18
|
export const stepDuration = (seconds) => {
|
|
3
19
|
const whole = Math.max(0, Math.round(seconds));
|
|
4
20
|
if (whole < 60) {
|
|
@@ -23,6 +39,7 @@ export class LogBuffer {
|
|
|
23
39
|
this.lines = [];
|
|
24
40
|
this.dropped = 0;
|
|
25
41
|
this.limit = 0;
|
|
42
|
+
this.collapse = false;
|
|
26
43
|
this.parse = undefined;
|
|
27
44
|
this.steps = [];
|
|
28
45
|
this.openStep = undefined;
|
|
@@ -60,7 +77,10 @@ export class LogBuffer {
|
|
|
60
77
|
return finished;
|
|
61
78
|
}
|
|
62
79
|
replace(lines) {
|
|
63
|
-
this.lines = [
|
|
80
|
+
this.lines = [];
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
this.add(line);
|
|
83
|
+
}
|
|
64
84
|
this.dropped = 0;
|
|
65
85
|
this.partial = '';
|
|
66
86
|
this.returned = false;
|
|
@@ -72,6 +92,10 @@ export class LogBuffer {
|
|
|
72
92
|
this.stepStarted = undefined;
|
|
73
93
|
this.trim();
|
|
74
94
|
}
|
|
95
|
+
push(line) {
|
|
96
|
+
this.add(line);
|
|
97
|
+
this.trim();
|
|
98
|
+
}
|
|
75
99
|
clear() {
|
|
76
100
|
this.replace([]);
|
|
77
101
|
}
|
|
@@ -126,7 +150,7 @@ export class LogBuffer {
|
|
|
126
150
|
this.stepEnded = at ?? this.stepEnded;
|
|
127
151
|
}
|
|
128
152
|
this.runs.set(line, line.text === raw ? flow.runs : parseAnsi(line.text, this.lineStyle).runs);
|
|
129
|
-
this.
|
|
153
|
+
this.add(line);
|
|
130
154
|
}
|
|
131
155
|
}
|
|
132
156
|
this.lineStyle = flow.style;
|
|
@@ -166,6 +190,19 @@ export class LogBuffer {
|
|
|
166
190
|
this.steps = this.steps.map((each, place) => (place === index ? closed : each));
|
|
167
191
|
this.openStep = undefined;
|
|
168
192
|
}
|
|
193
|
+
add(line) {
|
|
194
|
+
const last = this.lines.at(-1);
|
|
195
|
+
if (!this.collapse || !last || !repeats(line, last)) {
|
|
196
|
+
this.lines.push(line);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const folded = { ...last, count: (last.count ?? 1) + (line.count ?? 1) };
|
|
200
|
+
const runs = this.runs.get(last);
|
|
201
|
+
if (runs) {
|
|
202
|
+
this.runs.set(folded, runs);
|
|
203
|
+
}
|
|
204
|
+
this.lines[this.lines.length - 1] = folded;
|
|
205
|
+
}
|
|
169
206
|
trim() {
|
|
170
207
|
const over = this.limit > 0 ? this.lines.length - this.limit : 0;
|
|
171
208
|
if (over > 0) {
|
package/dist/log/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { type LogRun, type LogStyle, parseAnsi, stripAnsi } from './ansi.js';
|
|
2
|
-
export type
|
|
2
|
+
export { type LogGroup, type LogLevel, type LogLine, type LogParsed, type LogParser, type LogStatus, levelSaid, } from './buffer.js';
|
|
3
3
|
export { HelixHeadlessLogGroup } from './group/log-group.js';
|
|
4
4
|
export { HelixHeadlessLogLine, type LogTimes } from './line/log-line.js';
|
|
5
5
|
export { parseGitHubActions, parseVercel } from './parsers.js';
|
package/dist/log/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { parseAnsi, stripAnsi } from './ansi.js';
|
|
2
|
+
export { levelSaid, } from './buffer.js';
|
|
2
3
|
export { HelixHeadlessLogGroup } from './group/log-group.js';
|
|
3
4
|
export { HelixHeadlessLogLine } from './line/log-line.js';
|
|
4
5
|
export { parseGitHubActions, parseVercel } from './parsers.js';
|
|
@@ -13,8 +13,10 @@ export type LogTimes = 'absolute' | 'relative';
|
|
|
13
13
|
* and tells it whether to show its number and its time.
|
|
14
14
|
*
|
|
15
15
|
* `level` says how much a line matters, and the styled tier draws `warn` and
|
|
16
|
-
* `error` lines apart.
|
|
17
|
-
*
|
|
16
|
+
* `error` lines apart. A line at `debug`, `warn` or `error` says its level in
|
|
17
|
+
* words before its text, so a reader moving through the log hears it as well
|
|
18
|
+
* as the color shows it. The text is the line's own content, so you can mark
|
|
19
|
+
* it up yourself.
|
|
18
20
|
*
|
|
19
21
|
* @since 0.200.0
|
|
20
22
|
* @status experimental
|
|
@@ -22,6 +24,9 @@ export type LogTimes = 'absolute' | 'relative';
|
|
|
22
24
|
*
|
|
23
25
|
* @slot - The text of the line.
|
|
24
26
|
*
|
|
27
|
+
* @csspart count - How many times a line written again straight after itself was written, where the log's `collapse` folds repeats. Hidden from screen readers, which hear `count-label`.
|
|
28
|
+
* @csspart count-label - What a screen reader hears for the count, such as "3 times". Visually hidden in the styled tier.
|
|
29
|
+
* @csspart level - What a screen reader hears before the text for a line at `debug`, `warn` or `error`, such as "Warning:". Visually hidden in the styled tier.
|
|
25
30
|
* @csspart line - The row the number, the time, and the text sit in.
|
|
26
31
|
* @csspart line-debug - The row of a line at `debug`.
|
|
27
32
|
* @csspart line-error - The row of a line at `error`.
|
|
@@ -43,7 +48,7 @@ export type LogTimes = 'absolute' | 'relative';
|
|
|
43
48
|
* @cssproperty --log-run-background-color - Written on a `run` with a background, in the same form as `--log-run-color`.
|
|
44
49
|
* @cssproperty --log-run-color - Written on a `run` with a text color: one of the sixteen `--log-ansi-*` colors the log publishes, or the color itself.
|
|
45
50
|
*
|
|
46
|
-
* @cssstate filtered - The log's `query` does not match the line, so it draws nothing.
|
|
51
|
+
* @cssstate filtered - The log's `query` does not match the line, or its `level` is quieter than the log shows, so it draws nothing.
|
|
47
52
|
*/
|
|
48
53
|
export declare class HelixHeadlessLogLine extends HelixElement {
|
|
49
54
|
/** The key this family's words are registered under, its styled tag. */
|
|
@@ -52,8 +57,18 @@ export declare class HelixHeadlessLogLine extends HelixElement {
|
|
|
52
57
|
at: number | string;
|
|
53
58
|
/** How much the line matters. Absent is `info`. */
|
|
54
59
|
level?: LogLevel;
|
|
60
|
+
/**
|
|
61
|
+
* What a screen reader hears before the text of a line at each level, for a
|
|
62
|
+
* page in another language, keyed by level, such as `{ warn: 'Warnung:' }`.
|
|
63
|
+
* A line at `info` or `log` hears nothing extra, and nor does a line whose
|
|
64
|
+
* text already starts with its words, such as `Error: Cannot find module`.
|
|
65
|
+
* As an attribute it is that object as JSON.
|
|
66
|
+
*/
|
|
67
|
+
levelWords: Partial<Record<LogLevel, string>>;
|
|
55
68
|
/** What a screen reader hears for the link on a line's number. `{number}` is the number. */
|
|
56
69
|
lineLabel: string;
|
|
70
|
+
/** @internal Set by the log: how many times the line was written in a row, where `collapse` folds repeats. */
|
|
71
|
+
logCount: number;
|
|
57
72
|
/** @internal Set by the log: whether the query hides the line. */
|
|
58
73
|
logFiltered: boolean;
|
|
59
74
|
/** @internal Set by the log: whether the line belongs to a step. */
|
|
@@ -80,11 +95,23 @@ export declare class HelixHeadlessLogLine extends HelixElement {
|
|
|
80
95
|
logTimes: '' | LogTimes;
|
|
81
96
|
/** @internal Set by the log: the text as styled runs, for a line drawn from data. */
|
|
82
97
|
runs: readonly LogRun[] | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* What a screen reader hears after a line written more than once in a row.
|
|
100
|
+
* `{count}` is how many times, in the locale.
|
|
101
|
+
*/
|
|
102
|
+
timesLabel: string;
|
|
83
103
|
/**
|
|
84
104
|
* @internal Read by the log to find its lines among its children and inside
|
|
85
105
|
* its groups, which is cheaper than `instanceof`.
|
|
86
106
|
*/
|
|
87
107
|
readonly isLogLine = true;
|
|
108
|
+
/**
|
|
109
|
+
* How the text a page wrote starts, so a line whose text already says its
|
|
110
|
+
* level does not hear it twice. Read from the second update on, which a log
|
|
111
|
+
* asks for once it has hydrated, as the server has no children to read and
|
|
112
|
+
* the first render is the server's.
|
|
113
|
+
*/
|
|
114
|
+
private written;
|
|
88
115
|
protected willUpdate(changed: PropertyValues): void;
|
|
89
116
|
/**
|
|
90
117
|
* The number: a link where the log gave `line-href`, a button where the log
|
|
@@ -116,6 +143,10 @@ export declare class HelixHeadlessLogLine extends HelixElement {
|
|
|
116
143
|
* ink.
|
|
117
144
|
*/
|
|
118
145
|
private renderRuns;
|
|
146
|
+
/** The count of a folded line, shown as a number and heard as words. */
|
|
147
|
+
private renderCount;
|
|
148
|
+
/** Reads the new text where the line says a level, which it may already say. */
|
|
149
|
+
private readonly handleSlot;
|
|
119
150
|
protected render(): TemplateResult | typeof nothing;
|
|
120
151
|
}
|
|
121
152
|
declare global {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { __decorate } from "tslib";
|
|
2
|
-
import { customElement, formatWords, HelixElement, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
2
|
+
import { customElement, formatWords, HelixElement, words, wordsConverter, wordsLocale, } from '@fusion.dev/helix-core';
|
|
3
3
|
import { html, nothing } from 'lit';
|
|
4
4
|
import { property } from 'lit/decorators.js';
|
|
5
|
-
import { timeOf } from '../buffer.js';
|
|
5
|
+
import { levelSaid, timeOf } from '../buffer.js';
|
|
6
6
|
import { markMatches } from '../rows.js';
|
|
7
7
|
const NAME = /^[a-z-]+$/;
|
|
8
8
|
const elapsed = (seconds) => {
|
|
@@ -16,7 +16,13 @@ let HelixHeadlessLogLine = class HelixHeadlessLogLine extends HelixElement {
|
|
|
16
16
|
constructor() {
|
|
17
17
|
super(...arguments);
|
|
18
18
|
this.at = '';
|
|
19
|
+
this.levelWords = {
|
|
20
|
+
debug: 'Debug:',
|
|
21
|
+
error: 'Error:',
|
|
22
|
+
warn: 'Warning:',
|
|
23
|
+
};
|
|
19
24
|
this.lineLabel = 'Line {number}';
|
|
25
|
+
this.logCount = 1;
|
|
20
26
|
this.logFiltered = false;
|
|
21
27
|
this.logGrouped = false;
|
|
22
28
|
this.logHref = '';
|
|
@@ -30,12 +36,22 @@ let HelixHeadlessLogLine = class HelixHeadlessLogLine extends HelixElement {
|
|
|
30
36
|
this.logStop = false;
|
|
31
37
|
this.logTimes = '';
|
|
32
38
|
this.runs = undefined;
|
|
39
|
+
this.timesLabel = '{count} times';
|
|
33
40
|
this.isLogLine = true;
|
|
41
|
+
this.written = '';
|
|
42
|
+
this.handleSlot = () => {
|
|
43
|
+
if (this.level !== undefined && this.levelWords[this.level]) {
|
|
44
|
+
this.requestUpdate();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
34
47
|
}
|
|
35
48
|
static { this.words = 'helix-log'; }
|
|
36
49
|
willUpdate(changed) {
|
|
37
50
|
super.willUpdate(changed);
|
|
38
51
|
this.toggleState('filtered', this.logFiltered);
|
|
52
|
+
if (this.hasUpdated && !this.runs) {
|
|
53
|
+
this.written = (this.textContent ?? '').trimStart().slice(0, 40);
|
|
54
|
+
}
|
|
39
55
|
}
|
|
40
56
|
renderNumber() {
|
|
41
57
|
if (!this.logNumbers || this.logNumber <= 0) {
|
|
@@ -83,7 +99,7 @@ let HelixHeadlessLogLine = class HelixHeadlessLogLine extends HelixElement {
|
|
|
83
99
|
return html `<time part="time" datetime=${new Date(time).toISOString()}>${reads}</time>`;
|
|
84
100
|
}
|
|
85
101
|
renderText() {
|
|
86
|
-
return html `<slot>${this.runs ? this.renderRuns() : nothing}</slot>`;
|
|
102
|
+
return html `<slot @slotchange=${this.handleSlot}>${this.runs ? this.renderRuns() : nothing}</slot>`;
|
|
87
103
|
}
|
|
88
104
|
renderRuns() {
|
|
89
105
|
return markMatches(this.runs ?? [], this.logQuery).map((piece) => {
|
|
@@ -106,14 +122,23 @@ let HelixHeadlessLogLine = class HelixHeadlessLogLine extends HelixElement {
|
|
|
106
122
|
return piece.match ? html `<mark part="match">${run}</mark>` : run;
|
|
107
123
|
});
|
|
108
124
|
}
|
|
125
|
+
renderCount() {
|
|
126
|
+
if (this.logCount < 2) {
|
|
127
|
+
return nothing;
|
|
128
|
+
}
|
|
129
|
+
return html ` <span part="count" aria-hidden="true">${this.logCount}</span><span part="count-label"
|
|
130
|
+
>${formatWords(this, 'timesLabel', { count: this.logCount })}</span
|
|
131
|
+
>`;
|
|
132
|
+
}
|
|
109
133
|
render() {
|
|
110
134
|
if (this.logFiltered) {
|
|
111
135
|
return nothing;
|
|
112
136
|
}
|
|
113
137
|
const level = this.level;
|
|
138
|
+
const said = levelSaid(level === undefined ? undefined : this.levelWords[level], this.runs ? this.runs.map((run) => run.text).join('') : this.written);
|
|
114
139
|
return html `<div
|
|
115
|
-
part="line${level ? { debug: ' line-debug', error: ' line-error', info: '', warn: ' line-warn' }[level] : ''}${this.logMarked ? ' line-marked' : ''}${this.logSelected ? ' line-selected' : ''}${this.logGrouped ? ' line-grouped' : ''}"
|
|
116
|
-
>${this.renderNumber()}${this.renderTime()}<span part="text">${this.renderText()}</span
|
|
140
|
+
part="line${level ? { debug: ' line-debug', error: ' line-error', info: '', log: '', warn: ' line-warn' }[level] : ''}${this.logMarked ? ' line-marked' : ''}${this.logSelected ? ' line-selected' : ''}${this.logGrouped ? ' line-grouped' : ''}"
|
|
141
|
+
>${this.renderNumber()}${this.renderTime()}${said ? html `<span part="level">${said} </span>` : nothing}<span part="text">${this.renderText()}</span>${this.renderCount()}</div>`;
|
|
117
142
|
}
|
|
118
143
|
};
|
|
119
144
|
__decorate([
|
|
@@ -122,10 +147,17 @@ __decorate([
|
|
|
122
147
|
__decorate([
|
|
123
148
|
property({ reflect: true })
|
|
124
149
|
], HelixHeadlessLogLine.prototype, "level", void 0);
|
|
150
|
+
__decorate([
|
|
151
|
+
words,
|
|
152
|
+
property({ attribute: 'level-words', converter: wordsConverter })
|
|
153
|
+
], HelixHeadlessLogLine.prototype, "levelWords", void 0);
|
|
125
154
|
__decorate([
|
|
126
155
|
words,
|
|
127
156
|
property({ attribute: 'line-label' })
|
|
128
157
|
], HelixHeadlessLogLine.prototype, "lineLabel", void 0);
|
|
158
|
+
__decorate([
|
|
159
|
+
property({ attribute: false })
|
|
160
|
+
], HelixHeadlessLogLine.prototype, "logCount", void 0);
|
|
129
161
|
__decorate([
|
|
130
162
|
property({ attribute: false })
|
|
131
163
|
], HelixHeadlessLogLine.prototype, "logFiltered", void 0);
|
|
@@ -165,6 +197,10 @@ __decorate([
|
|
|
165
197
|
__decorate([
|
|
166
198
|
property({ attribute: false })
|
|
167
199
|
], HelixHeadlessLogLine.prototype, "runs", void 0);
|
|
200
|
+
__decorate([
|
|
201
|
+
words,
|
|
202
|
+
property({ attribute: 'times-label' })
|
|
203
|
+
], HelixHeadlessLogLine.prototype, "timesLabel", void 0);
|
|
168
204
|
HelixHeadlessLogLine = __decorate([
|
|
169
205
|
customElement('helix-headless-log-line')
|
|
170
206
|
], HelixHeadlessLogLine);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HelixElement } from '@fusion.dev/helix-core';
|
|
2
2
|
import { type PropertyValues, type TemplateResult } from 'lit';
|
|
3
|
-
import { type LogGroup, type LogLine, type LogParser } from '../buffer.js';
|
|
3
|
+
import { type LogGroup, type LogLevel, type LogLine, type LogParser } from '../buffer.js';
|
|
4
4
|
import type { LogTimes } from '../line/log-line.js';
|
|
5
5
|
/** Text a log reads as it arrives: a stream, or anything `for await` reads. */
|
|
6
6
|
export type LogSource = AsyncIterable<string | Uint8Array> | ReadableStream<string | Uint8Array>;
|
|
@@ -117,9 +117,12 @@ declare const HelixHeadlessLog_base: (new (...args: any[]) => import("@fusion.de
|
|
|
117
117
|
* @csspart group-summary-running - The button of a step drawn from `groups` that is `running`.
|
|
118
118
|
* @csspart group-summary-skipped - The button of a step drawn from `groups` that was `skipped`.
|
|
119
119
|
* @csspart line - The row of a line drawn from data.
|
|
120
|
+
* @csspart line-count - How many times a folded line was written, under `collapse`. Hidden from screen readers, which hear `line-count-label`.
|
|
121
|
+
* @csspart line-count-label - What a screen reader hears for a folded line's count, such as "3 times". Visually hidden in the styled tier.
|
|
120
122
|
* @csspart line-debug - The row of a line drawn from data at `debug`.
|
|
121
123
|
* @csspart line-error - The row of a line drawn from data at `error`.
|
|
122
124
|
* @csspart line-grouped - The row of a line drawn from data that belongs to a step.
|
|
125
|
+
* @csspart line-level - What a screen reader hears before the text of a line drawn from data at `debug`, `warn` or `error`. Visually hidden in the styled tier.
|
|
123
126
|
* @csspart line-marked - The row of a line drawn from data that `mark-lines` names.
|
|
124
127
|
* @csspart line-match - Text in a line drawn from data that matches `query`.
|
|
125
128
|
* @csspart line-number - The number of a line drawn from data.
|
|
@@ -164,7 +167,7 @@ declare const HelixHeadlessLog_base: (new (...args: any[]) => import("@fusion.de
|
|
|
164
167
|
*
|
|
165
168
|
* @cssstate done - The run is over: `done` is set, or `source` ended.
|
|
166
169
|
* @cssstate empty - The log has no lines.
|
|
167
|
-
* @cssstate filtered - `query` is set, so only
|
|
170
|
+
* @cssstate filtered - `query` or `level` is set, so only the lines they keep are shown.
|
|
168
171
|
* @cssstate pinned - The log follows its newest line.
|
|
169
172
|
*/
|
|
170
173
|
export declare class HelixHeadlessLog extends HelixHeadlessLog_base {
|
|
@@ -176,6 +179,13 @@ export declare class HelixHeadlessLog extends HelixHeadlessLog_base {
|
|
|
176
179
|
static words: string;
|
|
177
180
|
/** How tall a row is assumed to be before one has been drawn, in pixels. */
|
|
178
181
|
static readonly ESTIMATE = 20;
|
|
182
|
+
/**
|
|
183
|
+
* Folds a line written again straight after itself into it, with a count,
|
|
184
|
+
* as a browser's console does, so a loop writing one warning a thousand
|
|
185
|
+
* times is one line. For lines written or given as data, read as they
|
|
186
|
+
* arrive; numbers, `line-href` and the selection count a folded line once.
|
|
187
|
+
*/
|
|
188
|
+
collapse: boolean;
|
|
179
189
|
/**
|
|
180
190
|
* Whether the log opens following its newest line, when `pinned` is not set.
|
|
181
191
|
* `default-pinned="false"` opens it where it starts.
|
|
@@ -202,6 +212,7 @@ export declare class HelixHeadlessLog extends HelixHeadlessLog_base {
|
|
|
202
212
|
errorLabel: string;
|
|
203
213
|
/** The log's name, read by screen readers, where `label` is empty. */
|
|
204
214
|
fallbackLabel: string;
|
|
215
|
+
set format(next: 'github-actions' | 'vercel' | undefined);
|
|
205
216
|
/**
|
|
206
217
|
* The format written text is in, read with a parser Helix ships:
|
|
207
218
|
* `github-actions` reads a GitHub Actions log's workflow commands into steps
|
|
@@ -209,7 +220,6 @@ export declare class HelixHeadlessLog extends HelixHeadlessLog_base {
|
|
|
209
220
|
* reads none. `parse` wins over it.
|
|
210
221
|
*/
|
|
211
222
|
get format(): 'github-actions' | 'vercel' | undefined;
|
|
212
|
-
set format(next: 'github-actions' | 'vercel' | undefined);
|
|
213
223
|
/**
|
|
214
224
|
* What a screen reader hears when a step finishes or fails. `{heading}` is
|
|
215
225
|
* the step's heading and `{status}` is `done` or `failed`.
|
|
@@ -225,6 +235,13 @@ export declare class HelixHeadlessLog extends HelixHeadlessLog_base {
|
|
|
225
235
|
groups: readonly LogGroup[];
|
|
226
236
|
/** The log's name, read by screen readers. */
|
|
227
237
|
label: string;
|
|
238
|
+
/**
|
|
239
|
+
* The quietest level shown: `info` hides `debug` and `log` lines, `warn`
|
|
240
|
+
* shows warnings and errors, and `error` errors only. Unset shows every line.
|
|
241
|
+
* Lines it hides are kept, so lowering it shows them again, and a step shows
|
|
242
|
+
* while it holds a line that shows.
|
|
243
|
+
*/
|
|
244
|
+
level?: LogLevel;
|
|
228
245
|
/**
|
|
229
246
|
* The most lines kept, or 0 to keep them all. Past it the oldest lines are
|
|
230
247
|
* let go, and the line above the log says how many.
|
|
@@ -382,6 +399,13 @@ export declare class HelixHeadlessLog extends HelixHeadlessLog_base {
|
|
|
382
399
|
* after the last one is the line still being written.
|
|
383
400
|
*/
|
|
384
401
|
write(chunk: string): void;
|
|
402
|
+
/**
|
|
403
|
+
* Adds one line as data, after the lines the log holds: its text, and its
|
|
404
|
+
* level, time and group where it has them. For lines that arrive one at a
|
|
405
|
+
* time with a level of their own, such as a console's, where `write` takes
|
|
406
|
+
* text.
|
|
407
|
+
*/
|
|
408
|
+
writeLine(line: LogLine): void;
|
|
385
409
|
/**
|
|
386
410
|
* Lets every line go, and forgets which groups the reader opened, keeping
|
|
387
411
|
* `groups`, `parse`, and `limit`.
|