@artooi/ag-ui-web-component 0.25.2 → 0.26.1

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.
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Draw a {@link ChartSpec} as SVG.
3
+ *
4
+ * Every node is built with `createElement`, never parsed from a string, so this
5
+ * path never reaches the markdown sanitiser and cannot be widened by model
6
+ * output. That is the argument for taking a *spec* rather than markup: the model
7
+ * chooses the numbers, this module chooses the DOM. It is why a chart can be
8
+ * shown on a surface that keeps `img` off by default, a model-controlled URL
9
+ * being a zero-click exfiltration channel.
10
+ *
11
+ * Hand-rolled rather than a charting library, and the difference is not
12
+ * marginal: the whole renderer costs single-digit kilobytes where a library
13
+ * costs roughly half this bundle again, in a component distributed over a CDN.
14
+ */
15
+
16
+ const SVG_NS = "http://www.w3.org/2000/svg";
17
+
18
+ /** One named series of numbers. */
19
+ export interface ChartSeries {
20
+ readonly label: string;
21
+ readonly points: readonly number[];
22
+ }
23
+
24
+ /** How a spec is drawn. */
25
+ export type ChartKind = "bar" | "line" | "pie" | "scatter" | "stacked";
26
+
27
+ /** A chart, as data. */
28
+ export interface ChartSpec {
29
+ readonly kind: ChartKind;
30
+ readonly title?: string;
31
+ readonly labels: readonly string[];
32
+ readonly series: readonly ChartSeries[];
33
+ }
34
+
35
+ const WIDTH = 480;
36
+ const HEIGHT = 220;
37
+ const PAD = { top: 20, right: 12, bottom: 30, left: 44 };
38
+ const PLOT_W = WIDTH - PAD.left - PAD.right;
39
+ const PLOT_H = HEIGHT - PAD.top - PAD.bottom;
40
+
41
+ // Read from the host's own palette rather than a fixed ramp: the component
42
+ // themes through custom properties everywhere else, and a chart that ignored
43
+ // that would be the one element a host could not restyle.
44
+ const SERIES_COLORS: readonly string[] = [
45
+ "var(--ag-ui-chart-1, #4f7cff)",
46
+ "var(--ag-ui-chart-2, #21b573)",
47
+ "var(--ag-ui-chart-3, #e0803c)",
48
+ "var(--ag-ui-chart-4, #b563d8)",
49
+ "var(--ag-ui-chart-5, #d84f6e)",
50
+ "var(--ag-ui-chart-6, #3ba7c4)",
51
+ ];
52
+
53
+ /** The colour for series `index`, wrapping when there are more series than colours. */
54
+ export function seriesColor(index: number): string {
55
+ // Cast rather than a `??` fallback: the modulo guarantees a hit, so a
56
+ // fallback would be a branch no test could ever reach.
57
+ return SERIES_COLORS[index % SERIES_COLORS.length] as string;
58
+ }
59
+
60
+ function el<K extends keyof SVGElementTagNameMap>(
61
+ name: K,
62
+ attrs: Record<string, string | number>,
63
+ ): SVGElementTagNameMap[K] {
64
+ const node = document.createElementNS(SVG_NS, name);
65
+ for (const [key, value] of Object.entries(attrs)) {
66
+ node.setAttribute(key, String(value));
67
+ }
68
+ return node;
69
+ }
70
+
71
+ function text(value: string, attrs: Record<string, string | number>): SVGTextElement {
72
+ const node = el("text", {
73
+ "font-size": 10,
74
+ fill: "currentColor",
75
+ "fill-opacity": 0.65,
76
+ ...attrs,
77
+ });
78
+ // `textContent`, so a model-supplied label is text and never markup.
79
+ node.textContent = value;
80
+ return node;
81
+ }
82
+
83
+ /** Every running subtotal a stack passes through, column by column. */
84
+ function stackRunning(spec: ChartSpec): number[] {
85
+ const seen: number[] = [];
86
+ spec.labels.forEach((_label, i) => {
87
+ let running = 0;
88
+ for (const series of spec.series) {
89
+ running += series.points[i] ?? 0;
90
+ seen.push(running);
91
+ }
92
+ });
93
+ return seen;
94
+ }
95
+
96
+ function extent(spec: ChartSpec): { min: number; max: number } {
97
+ // A stack's extent has to cover every *running subtotal*, not just the column
98
+ // totals: with mixed signs the running value swings wider than the total it
99
+ // ends on, and scaling to the total alone puts segments far off the canvas.
100
+ const values =
101
+ spec.kind === "stacked"
102
+ ? stackRunning(spec)
103
+ : spec.series.flatMap((series) => [...series.points]);
104
+ const max = Math.max(0, ...values);
105
+ const min = Math.min(0, ...values);
106
+ // A flat series would divide by zero when scaling; give it a nominal span so
107
+ // it draws as a flat line rather than vanishing.
108
+ return max === min ? { min, max: max + 1 } : { min, max };
109
+ }
110
+
111
+ function scaleY(value: number, min: number, max: number): number {
112
+ return PAD.top + PLOT_H - ((value - min) / (max - min)) * PLOT_H;
113
+ }
114
+
115
+ function bandCentre(index: number, count: number): number {
116
+ const step = PLOT_W / count;
117
+ return PAD.left + step * index + step / 2;
118
+ }
119
+
120
+ function drawAxes(svg: SVGSVGElement, spec: ChartSpec, min: number, max: number): void {
121
+ for (const value of [min, max]) {
122
+ const y = scaleY(value, min, max);
123
+ svg.appendChild(
124
+ el("line", {
125
+ x1: PAD.left,
126
+ y1: y,
127
+ x2: WIDTH - PAD.right,
128
+ y2: y,
129
+ stroke: "currentColor",
130
+ "stroke-opacity": value === min ? 0.35 : 0.12,
131
+ }),
132
+ );
133
+ svg.appendChild(
134
+ text(String(Math.round(value)), { x: PAD.left - 6, y: y + 4, "text-anchor": "end" }),
135
+ );
136
+ }
137
+ spec.labels.forEach((label, i) => {
138
+ svg.appendChild(
139
+ text(label, {
140
+ x: bandCentre(i, spec.labels.length),
141
+ y: HEIGHT - PAD.bottom + 16,
142
+ "text-anchor": "middle",
143
+ }),
144
+ );
145
+ });
146
+ }
147
+
148
+ function drawBars(svg: SVGSVGElement, spec: ChartSpec, min: number, max: number): void {
149
+ const step = PLOT_W / spec.labels.length;
150
+ const width = (step * 0.7) / spec.series.length;
151
+ const base = scaleY(min, min, max);
152
+ spec.series.forEach((series, s) => {
153
+ series.points.forEach((value, i) => {
154
+ const y = scaleY(value, min, max);
155
+ svg.appendChild(
156
+ el("rect", {
157
+ x: PAD.left + step * i + step * 0.15 + width * s,
158
+ y,
159
+ width,
160
+ height: Math.max(1, base - y),
161
+ fill: seriesColor(s),
162
+ rx: 2,
163
+ }),
164
+ );
165
+ });
166
+ });
167
+ }
168
+
169
+ function drawStacked(svg: SVGSVGElement, spec: ChartSpec, min: number, max: number): void {
170
+ const step = PLOT_W / spec.labels.length;
171
+ const width = step * 0.7;
172
+ // Indexed by the same `i` it was built from, so every read is a hit; cast
173
+ // rather than defaulting, which would add a branch nothing can reach.
174
+ const running = spec.labels.map(() => 0);
175
+ spec.series.forEach((series, s) => {
176
+ series.points.forEach((value, i) => {
177
+ // `?? 0` rather than a cast: `renderChart` is exported, so a caller can
178
+ // hand it a series carrying more points than there are labels, which
179
+ // `chartSpecFrom` would have refused. The cast that used to be here
180
+ // claimed that could not happen and wrote `y="NaN"` into the DOM when it
181
+ // did.
182
+ const from = running[i] ?? 0;
183
+ const to = from + value;
184
+ running[i] = to;
185
+ const y = scaleY(to, min, max);
186
+ svg.appendChild(
187
+ el("rect", {
188
+ x: PAD.left + step * i + step * 0.15,
189
+ y,
190
+ width,
191
+ height: Math.max(1, scaleY(from, min, max) - y),
192
+ fill: seriesColor(s),
193
+ }),
194
+ );
195
+ });
196
+ });
197
+ }
198
+
199
+ function drawLines(svg: SVGSVGElement, spec: ChartSpec, min: number, max: number): void {
200
+ spec.series.forEach((series, s) => {
201
+ const points = series.points
202
+ .map((value, i) => `${bandCentre(i, spec.labels.length)},${scaleY(value, min, max)}`)
203
+ .join(" ");
204
+ svg.appendChild(
205
+ el("polyline", {
206
+ points,
207
+ fill: "none",
208
+ stroke: seriesColor(s),
209
+ "stroke-width": 2,
210
+ "stroke-linejoin": "round",
211
+ }),
212
+ );
213
+ });
214
+ }
215
+
216
+ function drawScatter(svg: SVGSVGElement, spec: ChartSpec, min: number, max: number): void {
217
+ spec.series.forEach((series, s) => {
218
+ series.points.forEach((value, i) => {
219
+ svg.appendChild(
220
+ el("circle", {
221
+ cx: bandCentre(i, spec.labels.length),
222
+ cy: scaleY(value, min, max),
223
+ r: 4,
224
+ fill: seriesColor(s),
225
+ "fill-opacity": 0.85,
226
+ }),
227
+ );
228
+ });
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Pie draws the **first** series' points as shares of their own total, one
234
+ * wedge per label — the only kind whose slices are the labels rather than the
235
+ * series, so a second series has nowhere to go and is ignored rather than
236
+ * silently summed into the first.
237
+ */
238
+ function drawPie(svg: SVGSVGElement, points: readonly number[]): void {
239
+ const total = points.reduce((sum, value) => sum + value, 0);
240
+ const cx = WIDTH / 2;
241
+ const cy = PAD.top + PLOT_H / 2;
242
+ const r = Math.min(PLOT_W, PLOT_H) / 2;
243
+ if (total === 0) {
244
+ // Every share is zero, so there is no wedge to draw and a full circle would
245
+ // claim one slice owns everything. An outline says "nothing here" honestly.
246
+ svg.appendChild(
247
+ el("circle", { cx, cy, r, fill: "none", stroke: "currentColor", "stroke-opacity": 0.3 }),
248
+ );
249
+ return;
250
+ }
251
+ let angle = -Math.PI / 2;
252
+ points.forEach((value, i) => {
253
+ const sweep = (value / total) * Math.PI * 2;
254
+ const end = angle + sweep;
255
+ // A wedge of the whole circle cannot be drawn as one arc (start and end
256
+ // coincide, so the path collapses); draw it as a plain circle instead.
257
+ if (sweep >= Math.PI * 2) {
258
+ svg.appendChild(el("circle", { cx, cy, r, fill: seriesColor(i) }));
259
+ } else {
260
+ const x1 = cx + r * Math.cos(angle);
261
+ const y1 = cy + r * Math.sin(angle);
262
+ const x2 = cx + r * Math.cos(end);
263
+ const y2 = cy + r * Math.sin(end);
264
+ const large = sweep > Math.PI ? 1 : 0;
265
+ svg.appendChild(
266
+ el("path", {
267
+ d: `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} Z`,
268
+ fill: seriesColor(i),
269
+ }),
270
+ );
271
+ }
272
+ angle = end;
273
+ });
274
+ }
275
+
276
+ function buildLegend(entries: readonly string[]): HTMLDivElement | null {
277
+ if (entries.length < 2) {
278
+ return null;
279
+ }
280
+ const row = document.createElement("div");
281
+ row.className = "chart-legend";
282
+ row.setAttribute("part", "chart-legend");
283
+ entries.forEach((label, i) => {
284
+ const item = document.createElement("span");
285
+ item.className = "chart-legend-item";
286
+ const swatch = document.createElement("span");
287
+ swatch.className = "chart-legend-swatch";
288
+ swatch.style.background = seriesColor(i);
289
+ item.append(swatch, document.createTextNode(label));
290
+ row.appendChild(item);
291
+ });
292
+ return row;
293
+ }
294
+
295
+ /**
296
+ * Render one spec as a self-contained block, or `null` when it says nothing.
297
+ *
298
+ * A spec with no labels or no series is not drawn: an empty frame reads as
299
+ * "there is no data" when the truth is "the caller sent nothing", and the two
300
+ * deserve different answers.
301
+ */
302
+ export function renderChart(spec: ChartSpec): HTMLDivElement | null {
303
+ if (spec.labels.length === 0 || spec.series.length === 0) {
304
+ return null;
305
+ }
306
+ const block = document.createElement("div");
307
+ block.className = "chart-block";
308
+ block.setAttribute("part", "chart-block");
309
+
310
+ if (spec.title !== undefined && spec.title !== "") {
311
+ const heading = document.createElement("div");
312
+ heading.className = "chart-title";
313
+ heading.setAttribute("part", "chart-title");
314
+ heading.textContent = spec.title;
315
+ block.appendChild(heading);
316
+ }
317
+
318
+ const svg = el("svg", { viewBox: `0 0 ${WIDTH} ${HEIGHT}`, width: "100%", role: "img" });
319
+ svg.setAttribute("aria-label", spec.title ?? `${spec.kind} chart`);
320
+
321
+ if (spec.kind === "pie") {
322
+ // `series[0]` is guaranteed by the early return above; a pie's slices are
323
+ // its labels, so a second series has nowhere to go and is ignored rather
324
+ // than silently summed into the first. Negative shares are floored, since a
325
+ // wedge cannot sweep backwards.
326
+ const first = spec.series[0] as ChartSeries;
327
+ drawPie(
328
+ svg,
329
+ first.points.map((value) => Math.max(0, value)),
330
+ );
331
+ } else {
332
+ const { min, max } = extent(spec);
333
+ drawAxes(svg, spec, min, max);
334
+ if (spec.kind === "bar") {
335
+ drawBars(svg, spec, min, max);
336
+ } else if (spec.kind === "stacked") {
337
+ drawStacked(svg, spec, min, max);
338
+ } else if (spec.kind === "line") {
339
+ drawLines(svg, spec, min, max);
340
+ } else {
341
+ drawScatter(svg, spec, min, max);
342
+ }
343
+ }
344
+ block.appendChild(svg);
345
+
346
+ // Pie's slices are its labels; every other kind's are its series.
347
+ const legend = buildLegend(
348
+ spec.kind === "pie" ? spec.labels : spec.series.map((series) => series.label),
349
+ );
350
+ if (legend !== null) {
351
+ block.appendChild(legend);
352
+ }
353
+ return block;
354
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Narrow untrusted input into a {@link ChartSpec}.
3
+ *
4
+ * Both arrival routes are untrusted in the same way — a model writes one, a
5
+ * server the other — so neither is taken on shape. Kept out of the renderer
6
+ * because the renderer's job is drawing, and a value that reaches it has
7
+ * already been vouched for.
8
+ */
9
+
10
+ import type { ChartKind, ChartSpec } from "./chart_block.js";
11
+
12
+ const KINDS: readonly ChartKind[] = ["bar", "line", "pie", "scatter", "stacked"];
13
+
14
+ /**
15
+ * Most points a spec may carry, across every series.
16
+ *
17
+ * Not a taste limit. `Math.max(0, ...values)` throws `RangeError` on a large
18
+ * enough spread, and the renderer runs inside the history replay, where a throw
19
+ * abandons the replay and takes every later turn of the transcript with it —
20
+ * permanently, on every reload. A chart nobody can read is a far smaller
21
+ * problem than a conversation that silently loses its tail.
22
+ */
23
+ const MAX_POINTS = 20_000;
24
+
25
+ /**
26
+ * Whether a spec's *labels* are cheap enough to draw.
27
+ *
28
+ * `MAX_POINTS` bounds the data; this bounds the DOM. Every label produces an
29
+ * axis text node whatever the series count, so a spec well inside the point
30
+ * budget can still emit tens of thousands of nodes and block the main thread —
31
+ * again on every reload, since it is in the transcript. Kept separate because
32
+ * the two limits answer different questions and a single number cannot.
33
+ */
34
+ const MAX_LABELS = 2_000;
35
+
36
+ /**
37
+ * Largest magnitude a point may carry.
38
+ *
39
+ * `Number.isFinite` is not enough on its own: two finite extremes still give an
40
+ * infinite *range*, and `(value - min) / Infinity` is `NaN`, which reaches the
41
+ * DOM as `y="NaN"`. Bounding the values bounds the range.
42
+ */
43
+ const MAX_MAGNITUDE = 1e15;
44
+
45
+ function asKind(value: unknown): ChartKind {
46
+ // Anything unrecognised falls back to `bar` rather than refusing the spec: an
47
+ // unknown kind is a caller reaching for a chart type we do not draw, and the
48
+ // data is still worth showing.
49
+ return KINDS.includes(value as ChartKind) ? (value as ChartKind) : "bar";
50
+ }
51
+
52
+ function asNumbers(value: unknown): number[] | null {
53
+ if (!Array.isArray(value)) {
54
+ return null;
55
+ }
56
+ const out: number[] = [];
57
+ for (const item of value) {
58
+ // `Number.isFinite` rather than `typeof === "number"`: JSON encoders render
59
+ // NaN and Infinity as nulls or strings depending on the encoder, and either
60
+ // would scale into a chart with no visible extent.
61
+ if (typeof item !== "number" || !Number.isFinite(item)) {
62
+ return null;
63
+ }
64
+ if (Math.abs(item) > MAX_MAGNITUDE) {
65
+ return null;
66
+ }
67
+ out.push(item);
68
+ }
69
+ return out;
70
+ }
71
+
72
+ function asStrings(value: unknown): string[] | null {
73
+ if (!Array.isArray(value)) {
74
+ return null;
75
+ }
76
+ // `every` over an index range rather than `Array.prototype.some`, which skips
77
+ // holes: a sparse array passed the old check and drew a chart with blank axis
78
+ // labels, which reads as a rendering bug rather than bad input.
79
+ for (let i = 0; i < value.length; i += 1) {
80
+ if (typeof value[i] !== "string") {
81
+ return null;
82
+ }
83
+ }
84
+ return value as string[];
85
+ }
86
+
87
+ /** A well-formed spec, or `null` for anything that cannot be drawn honestly. */
88
+ export function chartSpecFrom(value: unknown): ChartSpec | null {
89
+ if (typeof value !== "object" || value === null) {
90
+ return null;
91
+ }
92
+ const raw = value as Record<string, unknown>;
93
+ const labels = asStrings(raw["labels"]);
94
+ if (labels === null || !Array.isArray(raw["series"])) {
95
+ return null;
96
+ }
97
+
98
+ const series: { label: string; points: number[] }[] = [];
99
+ for (const entry of raw["series"]) {
100
+ if (typeof entry !== "object" || entry === null) {
101
+ return null;
102
+ }
103
+ const item = entry as Record<string, unknown>;
104
+ const points = asNumbers(item["points"]);
105
+ // A series with a different number of points than there are labels would
106
+ // silently misalign every value after the gap. A chart that is subtly wrong
107
+ // still reads as authoritative, which is worse than no chart at all.
108
+ if (points === null || points.length !== labels.length) {
109
+ return null;
110
+ }
111
+ series.push({ label: typeof item["label"] === "string" ? item["label"] : "", points });
112
+ }
113
+ if (series.length === 0) {
114
+ return null;
115
+ }
116
+ if (series.length * labels.length > MAX_POINTS || labels.length > MAX_LABELS) {
117
+ return null;
118
+ }
119
+
120
+ const kind = asKind(raw["kind"]);
121
+ const title = raw["title"];
122
+ // The key is omitted rather than set to `undefined`: `title` is genuinely
123
+ // optional and this tsconfig distinguishes absent from present-and-undefined.
124
+ return typeof title === "string" ? { kind, title, labels, series } : { kind, labels, series };
125
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The built-in `render_chart` tool: the agent-called route to a chart.
3
+ *
4
+ * A thin thing on purpose. It carries no `handler` of its own beyond reporting
5
+ * back to the model, because everything it does is drawing — which lives in
6
+ * `render`, the half the component replays. A consumer wanting charts gets this
7
+ * without writing a renderer; a consumer wanting something else writes their own
8
+ * tool against the same seam.
9
+ */
10
+
11
+ import type { ClientTool } from "../tools/client_tool_registry.js";
12
+ import { renderChart } from "./chart_block.js";
13
+ import { chartSpecFrom } from "./chart_spec_from.js";
14
+
15
+ /** The name the built-in chart tool registers under. */
16
+ export const CHART_TOOL_NAME = "render_chart";
17
+
18
+ function draw(args: Record<string, unknown>): HTMLDivElement | null {
19
+ const spec = chartSpecFrom(args);
20
+ return spec === null ? null : renderChart(spec);
21
+ }
22
+
23
+ /** Whether these arguments would produce a chart, without producing one. */
24
+ function drawable(args: Record<string, unknown>): boolean {
25
+ const spec = chartSpecFrom(args);
26
+ return spec !== null && spec.labels.length > 0 && spec.series.length > 0;
27
+ }
28
+
29
+ const REJECTED =
30
+ "chart not rendered: expected labels (strings) and series, each with one finite number per label";
31
+
32
+ /** The built-in chart tool. */
33
+ export function createChartTool(): ClientTool {
34
+ return {
35
+ name: CHART_TOOL_NAME,
36
+ description:
37
+ "Show a chart in the conversation. Supply the data and the page draws it. " +
38
+ "Every series must have exactly one point per label.",
39
+ parameters: {
40
+ type: "object",
41
+ properties: {
42
+ kind: { type: "string", enum: ["bar", "line", "pie", "scatter", "stacked"] },
43
+ title: { type: "string" },
44
+ labels: { type: "array", items: { type: "string" } },
45
+ series: {
46
+ type: "array",
47
+ items: {
48
+ type: "object",
49
+ properties: {
50
+ label: { type: "string" },
51
+ points: { type: "array", items: { type: "number" } },
52
+ },
53
+ required: ["points"],
54
+ },
55
+ },
56
+ },
57
+ required: ["labels", "series"],
58
+ "x-summary": "Draw a chart",
59
+ },
60
+ // Says what happened and nothing else; the drawing is `render`'s job. Told
61
+ // plainly when the arguments are unusable, because the model can fix that
62
+ // and retry — a silent no-op would leave it believing the chart is on screen.
63
+ // Answers on what will actually be drawn, not on what validated: a spec can
64
+ // pass validation and still have nothing to show, and reporting success
65
+ // then would leave the model believing a chart is on screen. Asks the
66
+ // question without building the chart, because `render` is about to build
67
+ // the same one a moment later and drawing it twice is pure waste on a spec
68
+ // large enough to matter.
69
+ handler: (args: Record<string, unknown>) => (drawable(args) ? "chart rendered" : REJECTED),
70
+ render: draw,
71
+ };
72
+ }
package/src/ui/styles.ts CHANGED
@@ -1528,6 +1528,50 @@ export const STYLES = `
1528
1528
  margin-top: 6px;
1529
1529
  }
1530
1530
 
1531
+ /* Charts.
1532
+ *
1533
+ * The SVG scales to the column and carries no colours of its own beyond the
1534
+ * series palette, so a host restyles it the same way it restyles everything
1535
+ * else. Series colours are custom properties with fallbacks rather than fixed
1536
+ * values, and the axis furniture inherits currentColor at low opacity so it
1537
+ * reads correctly in either theme without a second palette.
1538
+ */
1539
+ .chart-block {
1540
+ align-self: stretch;
1541
+ max-width: 100%;
1542
+ margin: 6px 0;
1543
+ color: var(--_fg);
1544
+ }
1545
+
1546
+ .chart-title {
1547
+ margin-bottom: 2px;
1548
+ font-size: 0.85em;
1549
+ font-weight: 600;
1550
+ opacity: 0.85;
1551
+ }
1552
+
1553
+ .chart-legend {
1554
+ display: flex;
1555
+ flex-wrap: wrap;
1556
+ gap: 4px 12px;
1557
+ margin-top: 4px;
1558
+ font-size: 0.78em;
1559
+ opacity: 0.75;
1560
+ }
1561
+
1562
+ .chart-legend-item {
1563
+ display: inline-flex;
1564
+ align-items: center;
1565
+ gap: 5px;
1566
+ }
1567
+
1568
+ .chart-legend-swatch {
1569
+ width: 9px;
1570
+ height: 9px;
1571
+ border-radius: 2px;
1572
+ flex: 0 0 auto;
1573
+ }
1574
+
1531
1575
  .run-notice {
1532
1576
  display: inline-flex;
1533
1577
  align-items: center;
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.25.2";
1
+ export const VERSION: string = "0.26.1";