@celestia-island/hikari 0.37.0 → 0.38.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/package.json +1 -1
- package/src/components/HkContextRing.scss +123 -0
- package/src/components/HkContextRing.test.tsx +309 -0
- package/src/components/HkContextRing.tsx +276 -0
- package/src/components/HkModal.headerLead.test.tsx +97 -0
- package/src/components/HkModal.scss +18 -0
- package/src/components/HkModal.tsx +10 -2
- package/src/components/HkProgressBar.scss +16 -0
- package/src/components/HkProgressBar.test.tsx +86 -0
- package/src/components/HkProgressBar.tsx +61 -13
- package/src/components/HkProgressRing.scss +9 -0
- package/src/components/HkProgressRing.test.tsx +140 -0
- package/src/components/HkProgressRing.tsx +107 -15
- package/src/i18n/contextKeys.test.ts +52 -0
- package/src/i18n/locales/ar/chat.json +12 -1
- package/src/i18n/locales/de/chat.json +12 -1
- package/src/i18n/locales/en/chat.json +12 -1
- package/src/i18n/locales/es/chat.json +12 -1
- package/src/i18n/locales/fr/chat.json +12 -1
- package/src/i18n/locales/ja/chat.json +12 -1
- package/src/i18n/locales/ko/chat.json +12 -1
- package/src/i18n/locales/pt/chat.json +12 -1
- package/src/i18n/locales/ru/chat.json +12 -1
- package/src/i18n/locales/zh-Hans/chat.json +12 -1
- package/src/i18n/locales/zh-Hant/chat.json +12 -1
- package/src/index.ts +1 -0
- package/src/tokens.scss +19 -0
package/package.json
CHANGED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Context-usage ring + hover popover (upstreamed for shittim-chest's
|
|
2
|
+
// session context meter). Layout/typography ride the theme tokens; the
|
|
3
|
+
// per-component segment colors are inline styles built from the
|
|
4
|
+
// `--context-<key>` CSS-variable family supplied by the host palette.
|
|
5
|
+
@use "./hikari-vars" as vars;
|
|
6
|
+
|
|
7
|
+
.hk-ctx-ring {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
cursor: pointer;
|
|
11
|
+
border-radius: var(--radius-full, 999px);
|
|
12
|
+
-webkit-tap-highlight-color: transparent;
|
|
13
|
+
|
|
14
|
+
&:focus-visible {
|
|
15
|
+
outline: 2px solid rgb(var(--color-primary));
|
|
16
|
+
outline-offset: 2px;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Center label rendered through HProgressRing's overlay slot; font size is
|
|
21
|
+
// set inline from the ring `size` prop.
|
|
22
|
+
.hk-ctx-ring-center {
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
line-height: 1;
|
|
25
|
+
color: rgb(var(--color-text));
|
|
26
|
+
font-variant-numeric: tabular-nums;
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
user-select: none;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.hk-ctx-pop {
|
|
33
|
+
width: 18rem;
|
|
34
|
+
padding: var(--space-12, 0.75rem);
|
|
35
|
+
font-size: var(--text-xs, 0.75rem);
|
|
36
|
+
color: rgb(var(--color-text));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.hk-ctx-pop-model {
|
|
40
|
+
display: flex;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.hk-ctx-pop-total {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: baseline;
|
|
46
|
+
gap: var(--space-6, 0.375rem);
|
|
47
|
+
margin-top: var(--space-8, 0.5rem);
|
|
48
|
+
margin-bottom: var(--space-8, 0.5rem);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.hk-ctx-pop-total-label {
|
|
52
|
+
color: rgb(var(--color-muted));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.hk-ctx-pop-total-num {
|
|
56
|
+
font-family: var(--font-mono, vars.$hikari-font-family-mono);
|
|
57
|
+
font-variant-numeric: tabular-nums;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.hk-ctx-pop-total-of {
|
|
61
|
+
color: rgb(var(--color-muted));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.hk-ctx-pop-bar {
|
|
65
|
+
margin-bottom: var(--space-8, 0.5rem);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.hk-ctx-legend {
|
|
69
|
+
list-style: none;
|
|
70
|
+
margin: 0;
|
|
71
|
+
padding: 0;
|
|
72
|
+
display: grid;
|
|
73
|
+
gap: 2px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.hk-ctx-legend-row {
|
|
77
|
+
display: grid;
|
|
78
|
+
grid-template-columns: auto 1fr auto auto;
|
|
79
|
+
gap: var(--space-6, 0.375rem);
|
|
80
|
+
align-items: baseline;
|
|
81
|
+
min-width: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.hk-ctx-legend-swatch {
|
|
85
|
+
width: 8px;
|
|
86
|
+
height: 8px;
|
|
87
|
+
border-radius: 2px;
|
|
88
|
+
align-self: center;
|
|
89
|
+
flex-shrink: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.hk-ctx-legend-label {
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
text-overflow: ellipsis;
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.hk-ctx-legend-tokens {
|
|
99
|
+
font-family: var(--font-mono, vars.$hikari-font-family-mono);
|
|
100
|
+
font-variant-numeric: tabular-nums;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.hk-ctx-legend-pct {
|
|
104
|
+
color: rgb(var(--color-muted));
|
|
105
|
+
font-variant-numeric: tabular-nums;
|
|
106
|
+
text-align: right;
|
|
107
|
+
min-width: 3ch;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.hk-ctx-est {
|
|
111
|
+
margin: var(--space-8, 0.5rem) 0 0;
|
|
112
|
+
padding: 0;
|
|
113
|
+
color: rgb(var(--color-muted));
|
|
114
|
+
font-size: var(--text-2xs, 0.625rem);
|
|
115
|
+
font-style: italic;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.hk-ctx-pop-empty {
|
|
119
|
+
margin-top: var(--space-8, 0.5rem);
|
|
120
|
+
color: rgb(var(--color-muted));
|
|
121
|
+
font-style: italic;
|
|
122
|
+
font-size: var(--text-2xs, 0.625rem);
|
|
123
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createApp, h, nextTick } from "vue";
|
|
3
|
+
|
|
4
|
+
// HPopover teleports to body and needs popup-manager runtime — stub it
|
|
5
|
+
// with a passthrough that renders its slot while open and records the
|
|
6
|
+
// interaction-relevant props (same pattern as HkModelTag.test.tsx).
|
|
7
|
+
vi.mock("@celestia-island/hikari", async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal<typeof import("@celestia-island/hikari")>();
|
|
9
|
+
const { defineComponent, h } = await import("vue");
|
|
10
|
+
const HPopoverStub = defineComponent({
|
|
11
|
+
name: "HPopover",
|
|
12
|
+
props: {
|
|
13
|
+
modelValue: { type: Boolean, default: false },
|
|
14
|
+
placement: { type: String, default: "bottom" },
|
|
15
|
+
sheetOnMobile: { type: Boolean, default: false },
|
|
16
|
+
title: { type: String, default: "" },
|
|
17
|
+
},
|
|
18
|
+
setup(props, { slots }) {
|
|
19
|
+
return () =>
|
|
20
|
+
props.modelValue
|
|
21
|
+
? h(
|
|
22
|
+
"div",
|
|
23
|
+
{
|
|
24
|
+
class: "popover-stub",
|
|
25
|
+
"data-placement": props.placement,
|
|
26
|
+
"data-sheet": String(props.sheetOnMobile),
|
|
27
|
+
},
|
|
28
|
+
slots.default?.(),
|
|
29
|
+
)
|
|
30
|
+
: null;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
return { ...actual, HPopover: HPopoverStub };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
import { HkContextRing } from "./HkContextRing";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* HkContextRing contract tests:
|
|
40
|
+
* - ring renders with a button role, model + usage aria-label and a
|
|
41
|
+
* floored center percentage
|
|
42
|
+
* - zero-token segments are dropped from ring, bar and legend
|
|
43
|
+
* - popover: click toggles, hover delays follow HkModelTag's choreography,
|
|
44
|
+
* sheetOnMobile is on, placement defaults to bottom-start
|
|
45
|
+
* - legend sorts by tokens desc with correct share math
|
|
46
|
+
* - contextWindow=null degrades to the gray ring + "–" center
|
|
47
|
+
*/
|
|
48
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
49
|
+
|
|
50
|
+
function mount(node: ReturnType<typeof h>) {
|
|
51
|
+
const container = document.createElement("div");
|
|
52
|
+
document.body.appendChild(container);
|
|
53
|
+
const app = createApp({ render: () => node });
|
|
54
|
+
app.mount(container);
|
|
55
|
+
mounts.push({ app, container });
|
|
56
|
+
return container;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
for (const { app, container } of mounts.splice(0)) {
|
|
61
|
+
app.unmount();
|
|
62
|
+
container.remove();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
function ringNode(props: Record<string, unknown>) {
|
|
67
|
+
return h(HkContextRing, props as never);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function baseProps(): Record<string, unknown> {
|
|
71
|
+
return {
|
|
72
|
+
model: "qwen3-coder-plus#8",
|
|
73
|
+
used: 42600,
|
|
74
|
+
contextWindow: 200000,
|
|
75
|
+
segments: [
|
|
76
|
+
{ key: "prompt", tokens: 20000 },
|
|
77
|
+
{ key: "thinking", tokens: 22600 },
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const openPopup = async (c: HTMLElement) => {
|
|
83
|
+
c.querySelector(".hk-ctx-ring")?.dispatchEvent(
|
|
84
|
+
new MouseEvent("click", { bubbles: true }),
|
|
85
|
+
);
|
|
86
|
+
await nextTick();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
describe("HkContextRing ring", () => {
|
|
90
|
+
it("renders a button-like trigger with model and floored usage in the label", () => {
|
|
91
|
+
const c = mount(ringNode(baseProps()));
|
|
92
|
+
const ring = c.querySelector(".hk-ctx-ring");
|
|
93
|
+
expect(ring?.getAttribute("role")).toBe("button");
|
|
94
|
+
expect(ring?.getAttribute("aria-label")).toContain("qwen3-coder-plus#8");
|
|
95
|
+
// 42600 / 200000 = 21.3% -> floored 21%
|
|
96
|
+
expect(ring?.getAttribute("aria-label")).toContain("21%");
|
|
97
|
+
expect(c.querySelector(".hk-ctx-ring-center")?.textContent).toBe("21%");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("shows 100+ once usage passes the window", () => {
|
|
101
|
+
const c = mount(ringNode({
|
|
102
|
+
model: "m",
|
|
103
|
+
used: 220_000,
|
|
104
|
+
contextWindow: 200_000,
|
|
105
|
+
segments: [],
|
|
106
|
+
}));
|
|
107
|
+
expect(c.querySelector(".hk-ctx-ring-center")?.textContent).toBe("100+");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("draws one arc per nonzero segment and none for zero-token segments", () => {
|
|
111
|
+
const c = mount(ringNode({
|
|
112
|
+
...baseProps(),
|
|
113
|
+
segments: [
|
|
114
|
+
{ key: "prompt", tokens: 20000 },
|
|
115
|
+
{ key: "tool", tokens: 0 },
|
|
116
|
+
{ key: "output", tokens: 22600 },
|
|
117
|
+
],
|
|
118
|
+
}));
|
|
119
|
+
const arcs = c.querySelectorAll(".hk-progress-ring-seg");
|
|
120
|
+
expect(arcs).toHaveLength(2);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("partitions the occupancy so estimated segments still fill up to the center pct", () => {
|
|
124
|
+
// Segments sum (20k) intentionally != used (60k): the raw tokens/window
|
|
125
|
+
// shares would end the fill at 10% while the center claims 30%. The
|
|
126
|
+
// arcs must instead split the whole 30% proportionally.
|
|
127
|
+
const c = mount(ringNode({
|
|
128
|
+
model: "m",
|
|
129
|
+
used: 60_000,
|
|
130
|
+
contextWindow: 200_000,
|
|
131
|
+
segments: [
|
|
132
|
+
{ key: "prompt", tokens: 5_000 },
|
|
133
|
+
{ key: "tool", tokens: 15_000 },
|
|
134
|
+
],
|
|
135
|
+
}));
|
|
136
|
+
const arcs = c.querySelectorAll(".hk-progress-ring-seg");
|
|
137
|
+
expect(arcs).toHaveLength(2);
|
|
138
|
+
const R = (28 - 3) / 2;
|
|
139
|
+
const C = 2 * Math.PI * R;
|
|
140
|
+
const drawn = [...arcs].reduce(
|
|
141
|
+
(acc, el) => acc + Number(el.getAttribute("stroke-dasharray")?.split(/\s+/)[0]),
|
|
142
|
+
0,
|
|
143
|
+
);
|
|
144
|
+
// drawn total = 30% of the circumference (within per-arc gap carve)
|
|
145
|
+
expect(drawn).toBeGreaterThan(0.25 * C);
|
|
146
|
+
expect(drawn).toBeLessThan(0.30 * C);
|
|
147
|
+
// Composition ratio preserved up to the inter-arc gap carve: the first
|
|
148
|
+
// arc (25% of the composition -> 7.5% of the ring) loses the 1%-of-ring
|
|
149
|
+
// gap, the last arc (75% -> 22.5%) keeps its full share.
|
|
150
|
+
const len0 = Number(arcs[0].getAttribute("stroke-dasharray")?.split(/\s+/)[0]);
|
|
151
|
+
const len1 = Number(arcs[1].getAttribute("stroke-dasharray")?.split(/\s+/)[0]);
|
|
152
|
+
expect(len0).toBeCloseTo((0.075 - 0.01) * C, 5);
|
|
153
|
+
expect(len1).toBeCloseTo(0.225 * C, 5);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("draws a single muted occupancy arc when composition is all zeros", () => {
|
|
157
|
+
const c = mount(ringNode({
|
|
158
|
+
...baseProps(),
|
|
159
|
+
segments: [
|
|
160
|
+
{ key: "prompt", tokens: 0 },
|
|
161
|
+
{ key: "tool", tokens: 0 },
|
|
162
|
+
],
|
|
163
|
+
}));
|
|
164
|
+
const arcs = c.querySelectorAll(".hk-progress-ring-seg");
|
|
165
|
+
expect(arcs).toHaveLength(1);
|
|
166
|
+
// The muted fallback carries the full 21.3% occupancy.
|
|
167
|
+
const R = (28 - 3) / 2;
|
|
168
|
+
const C = 2 * Math.PI * R;
|
|
169
|
+
const drawn = Number(arcs[0].getAttribute("stroke-dasharray")?.split(/\s+/)[0]);
|
|
170
|
+
expect(drawn).toBeCloseTo((42_600 / 200_000) * C, 5);
|
|
171
|
+
expect(arcs[0].getAttribute("style")).toContain("--context-free");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("degrades to a gray ring with a dash center when the window is null", () => {
|
|
175
|
+
const c = mount(ringNode({
|
|
176
|
+
model: "m",
|
|
177
|
+
used: 100,
|
|
178
|
+
contextWindow: null,
|
|
179
|
+
segments: [{ key: "prompt", tokens: 100 }],
|
|
180
|
+
}));
|
|
181
|
+
expect(c.querySelector(".hk-ctx-ring-center")?.textContent).toBe("–");
|
|
182
|
+
expect(c.querySelectorAll(".hk-progress-ring-seg")).toHaveLength(0);
|
|
183
|
+
expect(c.querySelector(".hk-progress-ring-track")).not.toBeNull();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("HkContextRing popover", () => {
|
|
188
|
+
it("opens and closes on click", async () => {
|
|
189
|
+
const c = mount(ringNode(baseProps()));
|
|
190
|
+
expect(c.querySelector(".hk-ctx-pop")).toBeNull();
|
|
191
|
+
await openPopup(c);
|
|
192
|
+
expect(c.querySelector(".hk-ctx-pop")).not.toBeNull();
|
|
193
|
+
expect(c.querySelector(".hk-ctx-pop-model")).not.toBeNull();
|
|
194
|
+
// model pill content is rendered inside the popup
|
|
195
|
+
expect(c.querySelector(".hk-ctx-pop-model")?.textContent).toContain("qwen3-coder-plus");
|
|
196
|
+
await openPopup(c);
|
|
197
|
+
expect(c.querySelector(".hk-ctx-pop")).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("opens on hover after the 250ms delay and closes on leave after 120ms", async () => {
|
|
201
|
+
vi.useFakeTimers();
|
|
202
|
+
try {
|
|
203
|
+
const c = mount(ringNode(baseProps()));
|
|
204
|
+
const ring = c.querySelector(".hk-ctx-ring")!;
|
|
205
|
+
ring.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
|
206
|
+
vi.advanceTimersByTime(249);
|
|
207
|
+
await nextTick();
|
|
208
|
+
expect(c.querySelector(".hk-ctx-pop")).toBeNull();
|
|
209
|
+
vi.advanceTimersByTime(2);
|
|
210
|
+
await nextTick();
|
|
211
|
+
expect(c.querySelector(".hk-ctx-pop")).not.toBeNull();
|
|
212
|
+
|
|
213
|
+
ring.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
|
214
|
+
vi.advanceTimersByTime(119);
|
|
215
|
+
await nextTick();
|
|
216
|
+
expect(c.querySelector(".hk-ctx-pop")).not.toBeNull();
|
|
217
|
+
vi.advanceTimersByTime(2);
|
|
218
|
+
await nextTick();
|
|
219
|
+
expect(c.querySelector(".hk-ctx-pop")).toBeNull();
|
|
220
|
+
} finally {
|
|
221
|
+
vi.useRealTimers();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("toggles with Enter and Space", async () => {
|
|
226
|
+
const c = mount(ringNode(baseProps()));
|
|
227
|
+
const ring = c.querySelector(".hk-ctx-ring")!;
|
|
228
|
+
ring.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
|
229
|
+
await nextTick();
|
|
230
|
+
expect(c.querySelector(".hk-ctx-pop")).not.toBeNull();
|
|
231
|
+
ring.dispatchEvent(new KeyboardEvent("keydown", { key: " ", bubbles: true }));
|
|
232
|
+
await nextTick();
|
|
233
|
+
expect(c.querySelector(".hk-ctx-pop")).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("asks HPopover for the mobile bottom sheet by default", async () => {
|
|
237
|
+
const c = mount(ringNode(baseProps()));
|
|
238
|
+
await openPopup(c);
|
|
239
|
+
const stub = c.querySelector(".popover-stub");
|
|
240
|
+
expect(stub?.getAttribute("data-sheet")).toBe("true");
|
|
241
|
+
expect(stub?.getAttribute("data-placement")).toBe("bottom-start");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("honours the placement prop", async () => {
|
|
245
|
+
const c = mount(ringNode({ ...baseProps(), placement: "top-end" }));
|
|
246
|
+
await openPopup(c);
|
|
247
|
+
expect(c.querySelector(".popover-stub")?.getAttribute("data-placement")).toBe("top-end");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("renders the segmented bar with the same data as the ring", async () => {
|
|
251
|
+
const c = mount(ringNode(baseProps()));
|
|
252
|
+
await openPopup(c);
|
|
253
|
+
const blocks = c.querySelectorAll(".hk-ctx-pop-bar .hk-progress-bar-seg");
|
|
254
|
+
expect(blocks).toHaveLength(2);
|
|
255
|
+
// 20000 / 200000 = 10%, 22600 / 200000 = 11.3%
|
|
256
|
+
expect((blocks[0] as HTMLElement).style.width).toBe("10%");
|
|
257
|
+
expect((blocks[1] as HTMLElement).style.width).toBe("11.3%");
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("HkContextRing legend", () => {
|
|
262
|
+
it("sorts rows by tokens desc and computes the window share", async () => {
|
|
263
|
+
const c = mount(ringNode({
|
|
264
|
+
model: "m",
|
|
265
|
+
used: 85000,
|
|
266
|
+
contextWindow: 200000,
|
|
267
|
+
segments: [
|
|
268
|
+
{ key: "prompt", tokens: 20000 },
|
|
269
|
+
{ key: "user", tokens: 5000 },
|
|
270
|
+
{ key: "thinking", tokens: 60000 },
|
|
271
|
+
],
|
|
272
|
+
}));
|
|
273
|
+
await openPopup(c);
|
|
274
|
+
const rows = c.querySelectorAll(".hk-ctx-legend-row");
|
|
275
|
+
expect(rows).toHaveLength(3);
|
|
276
|
+
const labels = Array.from(rows).map((r) => r.querySelector(".hk-ctx-legend-label")?.textContent);
|
|
277
|
+
expect(labels).toEqual(["Thinking", "Prompt", "User"]);
|
|
278
|
+
const tokens = Array.from(rows).map((r) => r.querySelector(".hk-ctx-legend-tokens")?.textContent);
|
|
279
|
+
expect(tokens).toEqual(["60.0k", "20.0k", "5.0k"]);
|
|
280
|
+
const pcts = Array.from(rows).map((r) => r.querySelector(".hk-ctx-legend-pct")?.textContent);
|
|
281
|
+
expect(pcts).toEqual(["30%", "10%", "2.5%"]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("resolves segment labels through the hikari::context i18n keys", async () => {
|
|
285
|
+
const c = mount(ringNode(baseProps()));
|
|
286
|
+
await openPopup(c);
|
|
287
|
+
const labels = Array.from(c.querySelectorAll(".hk-ctx-legend-label")).map((n) => n.textContent);
|
|
288
|
+
expect(labels).toContain("Thinking");
|
|
289
|
+
expect(labels).toContain("Prompt");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("lets an explicit label win over the i18n key", async () => {
|
|
293
|
+
const c = mount(ringNode({
|
|
294
|
+
...baseProps(),
|
|
295
|
+
segments: [{ key: "prompt", label: "System setup", tokens: 20000 }],
|
|
296
|
+
}));
|
|
297
|
+
await openPopup(c);
|
|
298
|
+
expect(c.querySelector(".hk-ctx-legend-label")?.textContent).toBe("System setup");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("shows the estimated footnote only when estimated is set", async () => {
|
|
302
|
+
const withEst = mount(ringNode({ ...baseProps(), estimated: true }));
|
|
303
|
+
await openPopup(withEst);
|
|
304
|
+
expect(withEst.querySelector(".hk-ctx-est")).not.toBeNull();
|
|
305
|
+
const without = mount(ringNode(baseProps()));
|
|
306
|
+
await openPopup(without);
|
|
307
|
+
expect(without.querySelector(".hk-ctx-est")).toBeNull();
|
|
308
|
+
});
|
|
309
|
+
});
|