@2lab.ai/ui 0.0.0-stage → 0.2.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.
Files changed (60) hide show
  1. package/README.md +55 -2
  2. package/dist/agent-inbox.js +1061 -0
  3. package/dist/agent-inbox.js.map +1 -0
  4. package/dist/agent-queue.js +1610 -0
  5. package/dist/agent-queue.js.map +1 -0
  6. package/dist/agent-run.js +859 -0
  7. package/dist/agent-run.js.map +1 -0
  8. package/dist/ai-answer.js +687 -0
  9. package/dist/ai-answer.js.map +1 -0
  10. package/dist/components/agent-inbox/agent-inbox.d.ts +158 -0
  11. package/dist/components/agent-inbox/index.d.ts +2 -0
  12. package/dist/components/agent-inbox/styles.d.ts +6 -0
  13. package/dist/components/agent-queue/agent-queue.d.ts +41 -0
  14. package/dist/components/agent-queue/index.d.ts +6 -0
  15. package/dist/components/agent-queue/logic.d.ts +261 -0
  16. package/dist/components/agent-queue/strings.d.ts +473 -0
  17. package/dist/components/agent-queue/styles.d.ts +8 -0
  18. package/dist/components/agent-run/agent-run.d.ts +273 -0
  19. package/dist/components/agent-run/index.d.ts +2 -0
  20. package/dist/components/agent-run/styles.d.ts +6 -0
  21. package/dist/components/ai-answer/ai-answer.d.ts +92 -0
  22. package/dist/components/ai-answer/index.d.ts +2 -0
  23. package/dist/components/ai-answer/styles.d.ts +2 -0
  24. package/dist/components/context-window/context-window.d.ts +156 -0
  25. package/dist/components/context-window/index.d.ts +2 -0
  26. package/dist/components/context-window/styles.d.ts +2 -0
  27. package/dist/components/memory-panel/index.d.ts +2 -0
  28. package/dist/components/memory-panel/memory-panel.d.ts +101 -0
  29. package/dist/components/memory-panel/styles.d.ts +2 -0
  30. package/dist/components/model-picker/index.d.ts +2 -0
  31. package/dist/components/model-picker/model-picker.d.ts +136 -0
  32. package/dist/components/model-picker/morph.d.ts +9 -0
  33. package/dist/components/model-picker/styles.d.ts +6 -0
  34. package/dist/components/tool-call-card/index.d.ts +7 -0
  35. package/dist/components/tool-call-card/logic.d.ts +87 -0
  36. package/dist/components/tool-call-card/strings.d.ts +94 -0
  37. package/dist/components/tool-call-card/styles.d.ts +5 -0
  38. package/dist/components/tool-call-card/tool-call-card.d.ts +49 -0
  39. package/dist/components/tool-call-card/tool-call-group.d.ts +32 -0
  40. package/dist/context-window.js +384 -0
  41. package/dist/context-window.js.map +1 -0
  42. package/dist/core/element.d.ts +41 -0
  43. package/dist/core/format.d.ts +14 -0
  44. package/dist/core/html.d.ts +10 -0
  45. package/dist/core/tokens.d.ts +14 -0
  46. package/dist/format-BBGWXPzk.js +51 -0
  47. package/dist/format-BBGWXPzk.js.map +1 -0
  48. package/dist/html-elD6Bb_u.js +175 -0
  49. package/dist/html-elD6Bb_u.js.map +1 -0
  50. package/dist/index.d.ts +16 -0
  51. package/dist/index.js +129 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/memory-panel.js +1255 -0
  54. package/dist/memory-panel.js.map +1 -0
  55. package/dist/model-picker.js +858 -0
  56. package/dist/model-picker.js.map +1 -0
  57. package/dist/tokens.css +4 -0
  58. package/dist/tool-call-card.js +1046 -0
  59. package/dist/tool-call-card.js.map +1 -0
  60. package/package.json +79 -4
@@ -0,0 +1,384 @@
1
+ import { d as define, U as UiElement, f as escAttr, e as esc, p as prefersReducedMotion } from "./html-elD6Bb_u.js";
2
+ import { e as fmtTokens } from "./format-BBGWXPzk.js";
3
+ const styles = `
4
+ :host { --cw-c1: var(--accent); --cw-c2: color-mix(in srgb, var(--accent) 42%, var(--ib-pill)); --cw-c3: var(--purple); --cw-c4: var(--green);
5
+ --cw-free: var(--ib-pill); --cw-warn: var(--st-wait); --cw-warn-bg: color-mix(in srgb, var(--st-wait) 14%, transparent);
6
+ --cw-full-bg: color-mix(in srgb, var(--st-fail) 12%, transparent); --cw-ok: var(--green); }
7
+ .cw { container: cw / inline-size; max-width: 640px; position: relative; padding-bottom: 2px; font-size: 12.5px; color: var(--text-secondary); }
8
+ .cw-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 22px; }
9
+ .cw-title { font-size: 13px; font-weight: 600; color: var(--text); }
10
+ .cw-model { flex: none; max-width: 62%; height: 20px; padding: 0 7px; border-radius: 5px; background: var(--ib-pill); font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11px; line-height: 20px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
11
+ .cw-figure { display: flex; align-items: baseline; gap: 7px; margin-top: 6px; }
12
+ .cw-big { font-size: 24px; font-weight: 600; line-height: 30px; letter-spacing: -0.02em; color: var(--text); font-variant-numeric: tabular-nums; }
13
+ .cw-of { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--text-tertiary); }
14
+ .cw-pill { flex: none; padding: 0 6px; border-radius: 5px; line-height: 20px; font-variant-numeric: tabular-nums; color: var(--text-secondary); transition: color 300ms ease, background-color 300ms ease; }
15
+ .cw[data-level="warn"] .cw-pill { color: var(--cw-warn); background: var(--cw-warn-bg); }
16
+ .cw[data-level="full"] .cw-pill { color: var(--st-fail-text); background: var(--cw-full-bg); font-weight: 600; }
17
+ .cw-field { display: grid; grid-template-columns: repeat(36, minmax(0, 1fr)); gap: 2px; margin-top: 10px; }
18
+ .cw-cell { display: block; aspect-ratio: 1; border-radius: 2px; background: var(--c, var(--cw-free)); transition: background-color 380ms var(--ease-out), opacity 220ms ease, transform var(--dur-move) var(--ease-out), box-shadow 380ms var(--ease-out); transition-delay: calc(var(--i) * 2ms), 0s, 0s, 0s; }
19
+ /* Past the auto-compact point an empty cell is a hollow square: the session compacts before it fills. */
20
+ .cw-cell[data-past]:not([data-part]) { background: transparent; box-shadow: inset 0 0 0 1px var(--border); }
21
+ .cw-field[data-hot] .cw-cell[data-part]:not([data-on]) { opacity: 0.22; }
22
+ .cw-cell[data-on] { transform: scale(1.12); }
23
+ .cw-cell[data-busy] { opacity: 0.5; }
24
+ @keyframes cw-wave { 0%, 100% { opacity: 1; transform: none; } 50% { opacity: 0.28; transform: scale(0.72); } }
25
+ @keyframes cw-swap-in { from { opacity: 0; filter: blur(2px); } }
26
+ @keyframes cw-leave { to { opacity: 0; filter: blur(3px); transform: scale(0.92); } }
27
+ @keyframes cw-spin { to { transform: rotate(360deg); } }
28
+ @media (prefers-reduced-motion: no-preference) {
29
+ .cw-cell[data-busy] { animation: cw-wave 900ms ease-in-out infinite; animation-delay: calc(var(--i) * 15ms); }
30
+ .cw-spin { animation: cw-spin 700ms linear infinite; }
31
+ }
32
+ .cw-parts { list-style: none; margin: 8px 0 0; padding: 0; }
33
+ .cw-part, .cw-mark { display: flex; align-items: center; gap: 9px; min-height: 30px; margin: 0 -8px; padding: 0 8px; border-radius: 8px; transition: background-color var(--dur-press) ease, opacity 220ms ease; }
34
+ .cw-part[data-hot] { background: var(--ib-pill); }
35
+ .cw-part[data-dim], .cw-mark[data-dim] { opacity: 0.55; }
36
+ .cw-sw { flex: none; width: 7px; height: 7px; border-radius: 2px; }
37
+ .cw-sw.is-mark { background: transparent; box-shadow: inset 0 0 0 1px var(--text-tertiary); }
38
+ .cw-label { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 13px; color: var(--text); }
39
+ .cw-mark .cw-label { color: var(--text-secondary); }
40
+ .cw-val { flex: none; min-width: 52px; text-align: right; font-size: 13px; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
41
+ .cw-foot { display: flex; align-items: center; gap: 8px; min-height: 38px; margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border); }
42
+ .cw-say { flex: 1; min-width: 0; display: grid; }
43
+ .cw-say-t { grid-area: 1 / 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--text-secondary); font-variant-numeric: tabular-nums; animation: cw-swap-in 320ms ease both; }
44
+ .cw-say-t b { font-weight: 600; color: var(--text); }
45
+ .cw-say-t.is-warn { color: var(--cw-warn); }
46
+ .cw-say-t.is-full, .cw-say-t.is-fail { color: var(--st-fail-text); }
47
+ .cw-say-t.is-ok, .cw-say-t.is-ok b { color: var(--cw-ok); }
48
+ .cw-compact { display: inline-flex; flex: none; align-items: center; gap: 5px; min-height: 28px; padding: 0 9px 0 7px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); font: inherit; font-size: 12.5px; font-weight: 600; color: var(--text); white-space: nowrap; cursor: pointer; transition: background-color var(--dur-press) ease, color var(--dur-press) ease, border-color var(--dur-press) ease; }
49
+ .cw-compact:hover:not(:disabled) { background: var(--surface-raised); }
50
+ .cw-compact:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
51
+ .cw-compact:disabled { cursor: default; opacity: 0.5; }
52
+ .cw-compact[data-state="armed"] { border-color: var(--cw-warn); color: var(--cw-warn); }
53
+ .cw-compact[data-state="busy"], .cw-compact[data-state="done"] { opacity: 1; }
54
+ .cw-compact[data-state="done"] { border-color: transparent; background: transparent; color: var(--cw-ok); }
55
+ .cw-compact[data-leaving] { animation: cw-leave 400ms var(--ease-out) both; }
56
+ .cw-ci, .cw-ct { display: inline-flex; align-items: center; animation: cw-swap-in 300ms ease both; }
57
+ .cw-ci svg { width: 12px; height: 12px; }
58
+ .cw-spin { display: block; width: 12px; height: 12px; border: 1.5px solid var(--border); border-top-color: var(--text); border-radius: 50%; }
59
+ @container cw (max-width: 309px) {
60
+ .cw-field { grid-template-columns: repeat(24, minmax(0, 1fr)); }
61
+ .cw-model { max-width: 42%; }
62
+ .cw-compact { padding: 0 8px; }
63
+ .cw-ct { display: none; }
64
+ }
65
+ @container cw (min-width: 520px) { .cw-field { grid-template-columns: repeat(54, minmax(0, 1fr)); } }
66
+ @media (pointer: coarse), (max-width: 639px) { .cw-compact { min-height: 44px; } .cw-part, .cw-mark { min-height: 36px; } }
67
+ `;
68
+ const CELLS = 216;
69
+ const ARM_MS = 4e3;
70
+ const ROLL_MS = 640;
71
+ const STRINGS = {
72
+ en: {
73
+ title: "Context",
74
+ of: (w) => `/ ${w} tokens`,
75
+ share: (label, pct) => `${label} · ${pct}% of window`,
76
+ parts: {
77
+ read: "Cached context (read)",
78
+ write: "Newly cached (write)",
79
+ input: "Uncached input",
80
+ output: "Last output"
81
+ },
82
+ unsplit: { compact: "Context after compaction", unknown: "Context (unsplit)" },
83
+ mark: "Auto-compact point",
84
+ sr: (u, w, p, a) => `Context ${u} of ${w} tokens used, ${p}${a ? `, auto-compacts at ${a}` : ""}`,
85
+ left: (n) => `${n} until auto-compact`,
86
+ soon: (n) => `Auto-compact soon · ${n} left`,
87
+ over: "Past the auto-compact point",
88
+ free: (n) => `${n} left in window`,
89
+ pending: "Compacts on the next input",
90
+ sending: "Sending compact request…",
91
+ sent: "Compact requested — waiting",
92
+ queued: "Compact requested — runs after this turn",
93
+ observed: "Compaction observed",
94
+ saved: (n, a, b) => `Compaction observed · saved <b>${n}</b> (${a} → ${b})`,
95
+ failed: "Compact request failed",
96
+ armed: "Earlier turns become a summary",
97
+ armedSr: "Press again to compact — earlier turns become a summary",
98
+ act: { idle: "Compact now", armed: "Press again", sending: "Sending", sent: "Requested", done: "Compacted" },
99
+ actLabel: (l) => `${l} — compact context`
100
+ },
101
+ ko: {
102
+ title: "컨텍스트",
103
+ of: (w) => `/ ${w} 토큰`,
104
+ share: (label, pct) => `${label} · 창의 ${pct}%`,
105
+ parts: {
106
+ read: "이전 맥락 (캐시 읽기)",
107
+ write: "새로 캐시 (캐시 쓰기)",
108
+ input: "캐시 안 된 입력",
109
+ output: "마지막 출력"
110
+ },
111
+ unsplit: { compact: "압축 후 맥락", unknown: "맥락 (분류 없음)" },
112
+ mark: "자동 압축 지점",
113
+ sr: (u, w, p, a) => `컨텍스트 ${u} / ${w} 토큰 사용, ${p}${a ? `, 자동 압축 ${a}` : ""}`,
114
+ left: (n) => `자동 압축까지 ${n}`,
115
+ soon: (n) => `자동 압축 임박 · ${n} 남음`,
116
+ over: "자동 압축 지점을 넘음",
117
+ free: (n) => `창에 ${n} 남음`,
118
+ pending: "다음 입력 때 자동 압축",
119
+ sending: "압축 요청 보내는 중…",
120
+ sent: "압축 요청 보냄 — 압축을 기다리는 중",
121
+ queued: "압축 요청 보냄 — 지금 턴이 끝나면 압축",
122
+ observed: "새 압축 관측됨",
123
+ saved: (n, a, b) => `새 압축 관측됨 · <b>${n}</b> 줄임 (${a} → ${b})`,
124
+ failed: "압축 요청 실패",
125
+ armed: "지난 대화가 요약으로 줄어듭니다",
126
+ armedSr: "한 번 더 누르면 압축 — 지난 대화가 요약으로 줄어듭니다",
127
+ act: {
128
+ idle: "지금 압축",
129
+ armed: "한 번 더 눌러 압축",
130
+ sending: "보내는 중",
131
+ sent: "요청 보냄",
132
+ done: "압축 관측됨"
133
+ },
134
+ actLabel: (l) => `${l} — 컨텍스트 압축`
135
+ }
136
+ };
137
+ const PART_ORDER = [
138
+ ["read", "cacheRead", "var(--cw-c1)"],
139
+ ["write", "cacheWrite", "var(--cw-c2)"],
140
+ ["input", "input", "var(--cw-c3)"],
141
+ ["output", "output", "var(--cw-c4)"]
142
+ ];
143
+ const COMPACT_SVG = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 3l4 3.5L12 3M4 13l4-3.5 4 3.5"/></svg>';
144
+ const CHECK_SVG = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>';
145
+ const num = (v) => {
146
+ const n = Number(v);
147
+ return Number.isFinite(n) && n > 0 ? n : 0;
148
+ };
149
+ const short = (n) => fmtTokens(n);
150
+ function contextParts(d, t = STRINGS.en) {
151
+ if (d.unsplit) {
152
+ const total = num(d.unsplit.tokens);
153
+ return total > 0 ? [{ key: "total", label: t.unsplit[d.unsplit.kind] ?? t.unsplit.unknown, tokens: total, color: "var(--cw-c1)" }] : [];
154
+ }
155
+ const out = [];
156
+ for (const [key, field, color] of PART_ORDER) {
157
+ const tokens = num(d.parts?.[field]);
158
+ if (tokens > 0) out.push({ key, label: t.parts[key], tokens, color });
159
+ }
160
+ return out;
161
+ }
162
+ function cellMap(parts, limit, cells = CELLS) {
163
+ const out = [];
164
+ const per = limit > 0 ? limit / cells : 0;
165
+ let start = 0;
166
+ let cum = 0;
167
+ for (let i = 0; i < parts.length && per > 0; i++) {
168
+ cum += parts[i].tokens;
169
+ const end = Math.min(cells, Math.max(parts[i].tokens > 0 ? start + 1 : start, Math.round(cum / per)));
170
+ for (let j = start; j < end; j++) out[j] = i;
171
+ start = Math.max(start, end);
172
+ }
173
+ for (let k = start; k < cells; k++) out[k] = -1;
174
+ return out;
175
+ }
176
+ function markIndex(auto, limit, cells = CELLS) {
177
+ if (auto <= 0 || limit <= 0 || auto >= limit) return -1;
178
+ return Math.max(0, Math.min(cells, Math.round(auto / (limit / cells))));
179
+ }
180
+ function contextLevel(used, window2, auto, pending) {
181
+ if (pending) return "full";
182
+ const a = num(auto);
183
+ if (used > 0 && a > 0) return used / a >= 0.95 ? "full" : used / a >= 0.8 ? "warn" : "ok";
184
+ const pct = window2 > 0 ? used / window2 * 100 : 0;
185
+ return pct > 80 ? "full" : pct > 60 ? "warn" : "ok";
186
+ }
187
+ class UiContextWindow extends UiElement {
188
+ static styles = [styles];
189
+ static get observedAttributes() {
190
+ return ["lang"];
191
+ }
192
+ #armedAt = 0;
193
+ #armTimer = 0;
194
+ #hot = "";
195
+ #shown = -1;
196
+ #raf = 0;
197
+ attributeChangedCallback() {
198
+ this.requestRender();
199
+ }
200
+ get #t() {
201
+ return this.getAttribute("lang") === "ko" ? STRINGS.ko : STRINGS.en;
202
+ }
203
+ constructor() {
204
+ super();
205
+ this.root.addEventListener("pointerover", (e) => {
206
+ const pe = e;
207
+ if (pe.pointerType && pe.pointerType !== "mouse") return;
208
+ const el = e.target?.closest?.(".cw-part[data-part], .cw-cell[data-part]");
209
+ const key = el?.getAttribute("data-part") ?? "";
210
+ if (key !== this.#hot) {
211
+ this.#hot = key;
212
+ this.#applyHot();
213
+ }
214
+ });
215
+ this.root.addEventListener("pointerleave", () => {
216
+ if (!this.#hot) return;
217
+ this.#hot = "";
218
+ this.#applyHot();
219
+ });
220
+ this.root.addEventListener("click", (e) => {
221
+ if (e.target?.closest?.(".cw-compact")) this.#press();
222
+ });
223
+ this.root.addEventListener("keydown", (e) => {
224
+ const ke = e;
225
+ if (ke.key === "Escape" && this.#armedAt) {
226
+ ke.stopPropagation();
227
+ this.#disarm();
228
+ }
229
+ });
230
+ }
231
+ disconnectedCallback() {
232
+ clearTimeout(this.#armTimer);
233
+ cancelAnimationFrame(this.#raf);
234
+ }
235
+ #press() {
236
+ const d = this.data;
237
+ if (!d || d.canCompact !== true) return;
238
+ const st = d.compactState ?? "idle";
239
+ if (st === "sending" || st === "sent" || st === "queued") return;
240
+ if (this.#armedAt && Date.now() - this.#armedAt < ARM_MS) {
241
+ this.#disarm(false);
242
+ this.emit("ui-compact", {});
243
+ this.requestRender();
244
+ return;
245
+ }
246
+ this.#armedAt = Date.now();
247
+ clearTimeout(this.#armTimer);
248
+ this.#armTimer = window.setTimeout(() => this.#disarm(), ARM_MS);
249
+ this.requestRender();
250
+ }
251
+ #disarm(render = true) {
252
+ this.#armedAt = 0;
253
+ clearTimeout(this.#armTimer);
254
+ if (render) this.requestRender();
255
+ }
256
+ render() {
257
+ const d = this.data;
258
+ const t = this.#t;
259
+ const win = num(d?.window);
260
+ if (!d || !(win > 0)) return "";
261
+ const parts = contextParts(d, t);
262
+ const used = parts.reduce((s, p) => s + p.tokens, 0);
263
+ const auto = num(d.autoCompactAt);
264
+ const level = contextLevel(used, win, auto, d.compactPending);
265
+ const state = d.compactState ?? "idle";
266
+ const busy = state === "sending" || state === "sent" || state === "queued";
267
+ const map = cellMap(parts, win);
268
+ const mark = markIndex(auto, win);
269
+ let cells = "";
270
+ for (let n = 0; n < CELLS; n++) {
271
+ const p = map[n] >= 0 ? parts[map[n]] : null;
272
+ cells += `<i class="cw-cell" style="--i:${n}${p ? `;--c:${p.color}` : ""}"${p ? ` data-part="${p.key}"` : ""}${mark >= 0 && n >= mark ? " data-past" : ""}${p && busy ? " data-busy" : ""}></i>`;
273
+ }
274
+ let rows = "";
275
+ for (const p of parts) {
276
+ const share = p.tokens / win * 100;
277
+ rows += `<li class="cw-part" data-part="${p.key}" data-tok="${p.tokens}" data-of="${escAttr(
278
+ t.share(p.label, share.toFixed(share < 10 ? 1 : 0))
279
+ )}"><i class="cw-sw" style="background:${p.color}"></i><span class="cw-label">${esc(p.label)}</span><span class="cw-val">${fmtTokens(p.tokens)}</span></li>`;
280
+ }
281
+ if (mark >= 0)
282
+ rows += `<li class="cw-mark"><i class="cw-sw is-mark"></i><span class="cw-label">${esc(t.mark)}</span><span class="cw-val">${short(auto)}</span></li>`;
283
+ const pct = `${Math.round(Math.min(used / win * 100, 100))}%`;
284
+ const ofTotal = t.of(short(win));
285
+ const say = this.#say(d, used, auto, win, level, state, t);
286
+ return `<section class="cw" role="group" aria-labelledby="cw-title" data-level="${level}">
287
+ <div class="cw-head"><span class="cw-title" id="cw-title">${esc(t.title)}</span>${d.model ? `<span class="cw-model">${esc(d.model)}</span>` : ""}</div>
288
+ <div class="cw-figure"><span class="cw-big" aria-hidden="true" data-to="${used}">${fmtTokens(this.#shown >= 0 ? this.#shown : used)}</span><span class="cw-of" aria-hidden="true" data-total="${escAttr(ofTotal)}">${esc(ofTotal)}</span><span class="cw-pill" aria-hidden="true">${pct}</span><span class="sr-only">${esc(t.sr(fmtTokens(used), short(win), pct, auto > 0 ? short(auto) : ""))}</span></div>
289
+ <div class="cw-field" aria-hidden="true">${cells}</div>
290
+ <ul class="cw-parts">${rows}</ul>
291
+ <div class="cw-foot"><span class="cw-say"><span class="cw-say-t${say.tone ? ` is-${say.tone}` : ""}">${say.html}</span></span><span class="sr-only" aria-live="polite">${say.announce ? say.sr ?? say.html : ""}</span>${this.#action(d, state, used, t)}</div>
292
+ </section>`;
293
+ }
294
+ #say(d, used, auto, win, level, state, t) {
295
+ if (state === "sending") return { tone: "", html: esc(t.sending), announce: true };
296
+ if (state === "sent") return { tone: "", html: esc(t.sent), announce: true };
297
+ if (state === "queued") return { tone: "", html: esc(t.queued), announce: true };
298
+ if (this.#armedAt) return { tone: "warn", html: esc(t.armed), sr: esc(t.armedSr), announce: true };
299
+ if (state === "failed") return { tone: "fail", html: esc(t.failed), announce: true };
300
+ const lc = d.lastCompact;
301
+ if (state === "observed") {
302
+ if (lc && num(lc.preTokens) > num(lc.postTokens))
303
+ return {
304
+ tone: "ok",
305
+ html: t.saved(
306
+ short(num(lc.preTokens) - num(lc.postTokens)),
307
+ short(num(lc.preTokens)),
308
+ short(num(lc.postTokens))
309
+ ),
310
+ announce: true
311
+ };
312
+ return { tone: "ok", html: esc(t.observed), announce: true };
313
+ }
314
+ if (d.compactPending) return { tone: "full", html: esc(t.pending), announce: true };
315
+ const tone = level === "full" ? "full" : level === "warn" ? "warn" : "";
316
+ if (auto > 0) {
317
+ if (used >= auto) return { tone: "full", html: esc(t.over) };
318
+ if (level === "full") return { tone: "full", html: esc(t.soon(short(auto - used))) };
319
+ return { tone, html: esc(t.left(short(auto - used))) };
320
+ }
321
+ return { tone, html: esc(t.free(short(Math.max(0, win - used)))) };
322
+ }
323
+ #action(d, state, used, t) {
324
+ if (d.canCompact === void 0 || d.canCompact === false || !(used > 0)) return "";
325
+ const armed = !!this.#armedAt;
326
+ const st = state === "sending" || state === "sent" || state === "queued" ? "busy" : state === "observed" ? "done" : armed ? "armed" : "idle";
327
+ const label = state === "sending" ? t.act.sending : state === "sent" || state === "queued" ? t.act.sent : st === "done" ? t.act.done : st === "armed" ? t.act.armed : t.act.idle;
328
+ const icon = st === "busy" ? '<span class="cw-spin" aria-hidden="true"></span>' : st === "done" ? CHECK_SVG : COMPACT_SVG;
329
+ const why = typeof d.canCompact === "string" ? d.canCompact : "";
330
+ const off = st === "busy" || st === "done" || !!why;
331
+ return `<button type="button" class="cw-compact" data-key="cw-compact" data-state="${st}"${off ? " disabled" : ""}${st === "busy" ? ' aria-busy="true"' : ""}${why ? ` title="${escAttr(why)}"` : ""} aria-label="${escAttr(t.actLabel(label))}"><span class="cw-ci">${icon}</span><span class="cw-ct">${esc(label)}</span></button>`;
332
+ }
333
+ updated() {
334
+ this.#applyHot();
335
+ }
336
+ /** Hover lives outside the markup: the hot part's cells lift, its row lights, the figure rolls to the part. */
337
+ #applyHot() {
338
+ const cw = this.$(".cw");
339
+ if (!cw) return;
340
+ let key = this.#hot;
341
+ const row = key ? cw.querySelector(`.cw-part[data-part="${key}"]`) : null;
342
+ if (!row) key = "";
343
+ cw.querySelector(".cw-field")?.toggleAttribute("data-hot", !!key);
344
+ for (const c of Array.from(cw.querySelectorAll(".cw-cell[data-part]")))
345
+ c.toggleAttribute("data-on", !!key && c.getAttribute("data-part") === key);
346
+ for (const r of Array.from(cw.querySelectorAll(".cw-part, .cw-mark"))) {
347
+ const mine = !!key && r.getAttribute("data-part") === key;
348
+ r.toggleAttribute("data-hot", mine);
349
+ r.toggleAttribute("data-dim", !!key && !mine);
350
+ }
351
+ const of = cw.querySelector(".cw-of");
352
+ if (of) of.textContent = key ? row?.getAttribute("data-of") ?? "" : of.getAttribute("data-total") ?? "";
353
+ const big = cw.querySelector(".cw-big");
354
+ if (big) this.#roll(big, num(key ? row?.getAttribute("data-tok") : big.getAttribute("data-to")));
355
+ }
356
+ /** The big number rolls to its target (640ms, ease-out quart); first paint and reduced motion jump. */
357
+ #roll(el, to) {
358
+ cancelAnimationFrame(this.#raf);
359
+ const from = this.#shown >= 0 ? this.#shown : to;
360
+ if (from === to || prefersReducedMotion() || typeof requestAnimationFrame !== "function") {
361
+ this.#shown = to;
362
+ el.textContent = fmtTokens(to);
363
+ return;
364
+ }
365
+ const t0 = performance.now();
366
+ const step = (now) => {
367
+ const p = Math.min(1, (now - t0) / ROLL_MS);
368
+ this.#shown = from + (to - from) * (1 - (1 - p) ** 4);
369
+ el.textContent = fmtTokens(this.#shown);
370
+ this.#raf = p < 1 ? requestAnimationFrame(step) : 0;
371
+ };
372
+ this.#raf = requestAnimationFrame(step);
373
+ }
374
+ }
375
+ define("ui-context-window", UiContextWindow);
376
+ export {
377
+ CELLS,
378
+ UiContextWindow,
379
+ cellMap,
380
+ contextLevel,
381
+ contextParts,
382
+ markIndex
383
+ };
384
+ //# sourceMappingURL=context-window.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-window.js","sources":["../src/components/context-window/styles.ts","../src/components/context-window/context-window.ts"],"sourcesContent":["/** Ported from the soma-work dashboard (.prd/affectui/re-context-window.md D5–D19). */\nexport const styles = `\n:host { --cw-c1: var(--accent); --cw-c2: color-mix(in srgb, var(--accent) 42%, var(--ib-pill)); --cw-c3: var(--purple); --cw-c4: var(--green);\n --cw-free: var(--ib-pill); --cw-warn: var(--st-wait); --cw-warn-bg: color-mix(in srgb, var(--st-wait) 14%, transparent);\n --cw-full-bg: color-mix(in srgb, var(--st-fail) 12%, transparent); --cw-ok: var(--green); }\n.cw { container: cw / inline-size; max-width: 640px; position: relative; padding-bottom: 2px; font-size: 12.5px; color: var(--text-secondary); }\n.cw-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 22px; }\n.cw-title { font-size: 13px; font-weight: 600; color: var(--text); }\n.cw-model { flex: none; max-width: 62%; height: 20px; padding: 0 7px; border-radius: 5px; background: var(--ib-pill); font-family: ui-monospace, \"SF Mono\", Menlo, monospace; font-size: 11px; line-height: 20px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cw-figure { display: flex; align-items: baseline; gap: 7px; margin-top: 6px; }\n.cw-big { font-size: 24px; font-weight: 600; line-height: 30px; letter-spacing: -0.02em; color: var(--text); font-variant-numeric: tabular-nums; }\n.cw-of { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--text-tertiary); }\n.cw-pill { flex: none; padding: 0 6px; border-radius: 5px; line-height: 20px; font-variant-numeric: tabular-nums; color: var(--text-secondary); transition: color 300ms ease, background-color 300ms ease; }\n.cw[data-level=\"warn\"] .cw-pill { color: var(--cw-warn); background: var(--cw-warn-bg); }\n.cw[data-level=\"full\"] .cw-pill { color: var(--st-fail-text); background: var(--cw-full-bg); font-weight: 600; }\n.cw-field { display: grid; grid-template-columns: repeat(36, minmax(0, 1fr)); gap: 2px; margin-top: 10px; }\n.cw-cell { display: block; aspect-ratio: 1; border-radius: 2px; background: var(--c, var(--cw-free)); transition: background-color 380ms var(--ease-out), opacity 220ms ease, transform var(--dur-move) var(--ease-out), box-shadow 380ms var(--ease-out); transition-delay: calc(var(--i) * 2ms), 0s, 0s, 0s; }\n/* Past the auto-compact point an empty cell is a hollow square: the session compacts before it fills. */\n.cw-cell[data-past]:not([data-part]) { background: transparent; box-shadow: inset 0 0 0 1px var(--border); }\n.cw-field[data-hot] .cw-cell[data-part]:not([data-on]) { opacity: 0.22; }\n.cw-cell[data-on] { transform: scale(1.12); }\n.cw-cell[data-busy] { opacity: 0.5; }\n@keyframes cw-wave { 0%, 100% { opacity: 1; transform: none; } 50% { opacity: 0.28; transform: scale(0.72); } }\n@keyframes cw-swap-in { from { opacity: 0; filter: blur(2px); } }\n@keyframes cw-leave { to { opacity: 0; filter: blur(3px); transform: scale(0.92); } }\n@keyframes cw-spin { to { transform: rotate(360deg); } }\n@media (prefers-reduced-motion: no-preference) {\n .cw-cell[data-busy] { animation: cw-wave 900ms ease-in-out infinite; animation-delay: calc(var(--i) * 15ms); }\n .cw-spin { animation: cw-spin 700ms linear infinite; }\n}\n.cw-parts { list-style: none; margin: 8px 0 0; padding: 0; }\n.cw-part, .cw-mark { display: flex; align-items: center; gap: 9px; min-height: 30px; margin: 0 -8px; padding: 0 8px; border-radius: 8px; transition: background-color var(--dur-press) ease, opacity 220ms ease; }\n.cw-part[data-hot] { background: var(--ib-pill); }\n.cw-part[data-dim], .cw-mark[data-dim] { opacity: 0.55; }\n.cw-sw { flex: none; width: 7px; height: 7px; border-radius: 2px; }\n.cw-sw.is-mark { background: transparent; box-shadow: inset 0 0 0 1px var(--text-tertiary); }\n.cw-label { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 13px; color: var(--text); }\n.cw-mark .cw-label { color: var(--text-secondary); }\n.cw-val { flex: none; min-width: 52px; text-align: right; font-size: 13px; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }\n.cw-foot { display: flex; align-items: center; gap: 8px; min-height: 38px; margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border); }\n.cw-say { flex: 1; min-width: 0; display: grid; }\n.cw-say-t { grid-area: 1 / 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--text-secondary); font-variant-numeric: tabular-nums; animation: cw-swap-in 320ms ease both; }\n.cw-say-t b { font-weight: 600; color: var(--text); }\n.cw-say-t.is-warn { color: var(--cw-warn); }\n.cw-say-t.is-full, .cw-say-t.is-fail { color: var(--st-fail-text); }\n.cw-say-t.is-ok, .cw-say-t.is-ok b { color: var(--cw-ok); }\n.cw-compact { display: inline-flex; flex: none; align-items: center; gap: 5px; min-height: 28px; padding: 0 9px 0 7px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); font: inherit; font-size: 12.5px; font-weight: 600; color: var(--text); white-space: nowrap; cursor: pointer; transition: background-color var(--dur-press) ease, color var(--dur-press) ease, border-color var(--dur-press) ease; }\n.cw-compact:hover:not(:disabled) { background: var(--surface-raised); }\n.cw-compact:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n.cw-compact:disabled { cursor: default; opacity: 0.5; }\n.cw-compact[data-state=\"armed\"] { border-color: var(--cw-warn); color: var(--cw-warn); }\n.cw-compact[data-state=\"busy\"], .cw-compact[data-state=\"done\"] { opacity: 1; }\n.cw-compact[data-state=\"done\"] { border-color: transparent; background: transparent; color: var(--cw-ok); }\n.cw-compact[data-leaving] { animation: cw-leave 400ms var(--ease-out) both; }\n.cw-ci, .cw-ct { display: inline-flex; align-items: center; animation: cw-swap-in 300ms ease both; }\n.cw-ci svg { width: 12px; height: 12px; }\n.cw-spin { display: block; width: 12px; height: 12px; border: 1.5px solid var(--border); border-top-color: var(--text); border-radius: 50%; }\n@container cw (max-width: 309px) {\n .cw-field { grid-template-columns: repeat(24, minmax(0, 1fr)); }\n .cw-model { max-width: 42%; }\n .cw-compact { padding: 0 8px; }\n .cw-ct { display: none; }\n}\n@container cw (min-width: 520px) { .cw-field { grid-template-columns: repeat(54, minmax(0, 1fr)); } }\n@media (pointer: coarse), (max-width: 639px) { .cw-compact { min-height: 44px; } .cw-part, .cw-mark { min-height: 36px; } }\n`;\n","import { define, prefersReducedMotion, UiElement } from '../../core/element';\nimport { fmtTokens } from '../../core/format';\nimport { esc, escAttr } from '../../core/html';\nimport { styles } from './styles';\n\n/** What fills a model's context window, in prompt order. Tokens of the last request. */\nexport interface ContextParts {\n /** Cached prefix read back (cache read). */\n cacheRead?: number;\n /** Newly cached this request (cache write). */\n cacheWrite?: number;\n /** Uncached input tail. */\n input?: number;\n /** The last response. */\n output?: number;\n}\n\n/** Host-owned state of a compaction request. The component adds the two-press confirm itself. */\nexport type CompactState = 'idle' | 'sending' | 'sent' | 'queued' | 'observed' | 'failed';\n\nexport interface ContextWindowData {\n /** Model id shown as a chip (e.g. `opus-4-5[1m]`). */\n model?: string;\n /** Window size in tokens. Nothing renders without it. */\n window: number;\n parts?: ContextParts;\n /** The usage is one total that cannot be split into cache categories. */\n unsplit?: { tokens: number; kind: 'compact' | 'unknown' };\n /** Auto-compaction threshold in tokens (drawn on the field, drives the warning level). */\n autoCompactAt?: number;\n /** The next input will auto-compact. */\n compactPending?: boolean;\n /** Last compaction's before/after, shown once a compaction is observed. */\n lastCompact?: { preTokens: number; postTokens: number };\n /** `true` shows the compact action; a string shows it disabled with that reason. Default: hidden. */\n canCompact?: boolean | string;\n compactState?: CompactState;\n}\n\nexport type ContextLevel = 'ok' | 'warn' | 'full';\n\nexport const CELLS = 216;\nconst ARM_MS = 4000;\nconst ROLL_MS = 640;\n\nconst STRINGS = {\n en: {\n title: 'Context',\n of: (w: string) => `/ ${w} tokens`,\n share: (label: string, pct: string) => `${label} · ${pct}% of window`,\n parts: {\n read: 'Cached context (read)',\n write: 'Newly cached (write)',\n input: 'Uncached input',\n output: 'Last output',\n },\n unsplit: { compact: 'Context after compaction', unknown: 'Context (unsplit)' },\n mark: 'Auto-compact point',\n sr: (u: string, w: string, p: string, a: string) =>\n `Context ${u} of ${w} tokens used, ${p}${a ? `, auto-compacts at ${a}` : ''}`,\n left: (n: string) => `${n} until auto-compact`,\n soon: (n: string) => `Auto-compact soon · ${n} left`,\n over: 'Past the auto-compact point',\n free: (n: string) => `${n} left in window`,\n pending: 'Compacts on the next input',\n sending: 'Sending compact request…',\n sent: 'Compact requested — waiting',\n queued: 'Compact requested — runs after this turn',\n observed: 'Compaction observed',\n saved: (n: string, a: string, b: string) => `Compaction observed · saved <b>${n}</b> (${a} → ${b})`,\n failed: 'Compact request failed',\n armed: 'Earlier turns become a summary',\n armedSr: 'Press again to compact — earlier turns become a summary',\n act: { idle: 'Compact now', armed: 'Press again', sending: 'Sending', sent: 'Requested', done: 'Compacted' },\n actLabel: (l: string) => `${l} — compact context`,\n },\n ko: {\n title: '컨텍스트',\n of: (w: string) => `/ ${w} 토큰`,\n share: (label: string, pct: string) => `${label} · 창의 ${pct}%`,\n parts: {\n read: '이전 맥락 (캐시 읽기)',\n write: '새로 캐시 (캐시 쓰기)',\n input: '캐시 안 된 입력',\n output: '마지막 출력',\n },\n unsplit: { compact: '압축 후 맥락', unknown: '맥락 (분류 없음)' },\n mark: '자동 압축 지점',\n sr: (u: string, w: string, p: string, a: string) =>\n `컨텍스트 ${u} / ${w} 토큰 사용, ${p}${a ? `, 자동 압축 ${a}` : ''}`,\n left: (n: string) => `자동 압축까지 ${n}`,\n soon: (n: string) => `자동 압축 임박 · ${n} 남음`,\n over: '자동 압축 지점을 넘음',\n free: (n: string) => `창에 ${n} 남음`,\n pending: '다음 입력 때 자동 압축',\n sending: '압축 요청 보내는 중…',\n sent: '압축 요청 보냄 — 압축을 기다리는 중',\n queued: '압축 요청 보냄 — 지금 턴이 끝나면 압축',\n observed: '새 압축 관측됨',\n saved: (n: string, a: string, b: string) => `새 압축 관측됨 · <b>${n}</b> 줄임 (${a} → ${b})`,\n failed: '압축 요청 실패',\n armed: '지난 대화가 요약으로 줄어듭니다',\n armedSr: '한 번 더 누르면 압축 — 지난 대화가 요약으로 줄어듭니다',\n act: {\n idle: '지금 압축',\n armed: '한 번 더 눌러 압축',\n sending: '보내는 중',\n sent: '요청 보냄',\n done: '압축 관측됨',\n },\n actLabel: (l: string) => `${l} — 컨텍스트 압축`,\n },\n};\ntype Strings = (typeof STRINGS)['en'];\n\nconst PART_ORDER: Array<[key: 'read' | 'write' | 'input' | 'output', field: keyof ContextParts, color: string]> = [\n ['read', 'cacheRead', 'var(--cw-c1)'],\n ['write', 'cacheWrite', 'var(--cw-c2)'],\n ['input', 'input', 'var(--cw-c3)'],\n ['output', 'output', 'var(--cw-c4)'],\n];\n\nconst COMPACT_SVG =\n '<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 3l4 3.5L12 3M4 13l4-3.5 4 3.5\"/></svg>';\nconst CHECK_SVG =\n '<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg>';\n\ninterface Part {\n key: string;\n label: string;\n tokens: number;\n color: string;\n}\n\nconst num = (v: unknown): number => {\n const n = Number(v);\n return Number.isFinite(n) && n > 0 ? n : 0;\n};\nconst short = (n: number) => fmtTokens(n);\n\n/** Parts in prompt order; zero parts are left out; an unsplit total is one row. */\nexport function contextParts(d: ContextWindowData, t: Strings = STRINGS.en): Part[] {\n if (d.unsplit) {\n const total = num(d.unsplit.tokens);\n return total > 0\n ? [{ key: 'total', label: t.unsplit[d.unsplit.kind] ?? t.unsplit.unknown, tokens: total, color: 'var(--cw-c1)' }]\n : [];\n }\n const out: Part[] = [];\n for (const [key, field, color] of PART_ORDER) {\n const tokens = num(d.parts?.[field]);\n if (tokens > 0) out.push({ key, label: t.parts[key], tokens, color });\n }\n return out;\n}\n\n/** Cell → part index (-1 free): cumulative rounding in part order, every non-empty part ≥ 1 cell. */\nexport function cellMap(parts: Array<{ tokens: number }>, limit: number, cells = CELLS): number[] {\n const out: number[] = [];\n const per = limit > 0 ? limit / cells : 0;\n let start = 0;\n let cum = 0;\n for (let i = 0; i < parts.length && per > 0; i++) {\n cum += parts[i].tokens;\n const end = Math.min(cells, Math.max(parts[i].tokens > 0 ? start + 1 : start, Math.round(cum / per)));\n for (let j = start; j < end; j++) out[j] = i;\n start = Math.max(start, end);\n }\n for (let k = start; k < cells; k++) out[k] = -1;\n return out;\n}\n\n/** First cell past the auto-compact point, -1 when none inside the window. */\nexport function markIndex(auto: number, limit: number, cells = CELLS): number {\n if (auto <= 0 || limit <= 0 || auto >= limit) return -1;\n return Math.max(0, Math.min(cells, Math.round(auto / (limit / cells))));\n}\n\n/** Warning level against the auto-compact point (≥80% warn, ≥95% full), else against the window (>60 / >80%). */\nexport function contextLevel(used: number, window: number, auto?: number, pending?: boolean): ContextLevel {\n if (pending) return 'full';\n const a = num(auto);\n if (used > 0 && a > 0) return used / a >= 0.95 ? 'full' : used / a >= 0.8 ? 'warn' : 'ok';\n const pct = window > 0 ? (used / window) * 100 : 0;\n return pct > 80 ? 'full' : pct > 60 ? 'warn' : 'ok';\n}\n\n/**\n * `<ui-context-window>` — what fills the model's context: 216 cells (one per window/216 tokens) coloured by\n * cache category in prompt order, the auto-compact point on the field, a level against that point, and an\n * optional two-press \"Compact now\" action (`ui-compact`).\n */\nexport class UiContextWindow extends UiElement<ContextWindowData> {\n static override styles = [styles];\n static get observedAttributes() {\n return ['lang'];\n }\n\n #armedAt = 0;\n #armTimer = 0;\n #hot = '';\n #shown = -1;\n #raf = 0;\n\n attributeChangedCallback(): void {\n this.requestRender();\n }\n\n get #t(): Strings {\n return this.getAttribute('lang') === 'ko' ? STRINGS.ko : STRINGS.en;\n }\n\n constructor() {\n super();\n this.root.addEventListener('pointerover', (e) => {\n const pe = e as PointerEvent;\n if (pe.pointerType && pe.pointerType !== 'mouse') return;\n const el = (e.target as Element | null)?.closest?.('.cw-part[data-part], .cw-cell[data-part]');\n const key = el?.getAttribute('data-part') ?? '';\n if (key !== this.#hot) {\n this.#hot = key;\n this.#applyHot();\n }\n });\n this.root.addEventListener('pointerleave', () => {\n if (!this.#hot) return;\n this.#hot = '';\n this.#applyHot();\n });\n this.root.addEventListener('click', (e) => {\n if ((e.target as Element | null)?.closest?.('.cw-compact')) this.#press();\n });\n this.root.addEventListener('keydown', (e) => {\n const ke = e as KeyboardEvent;\n if (ke.key === 'Escape' && this.#armedAt) {\n ke.stopPropagation();\n this.#disarm();\n }\n });\n }\n\n override disconnectedCallback(): void {\n clearTimeout(this.#armTimer);\n cancelAnimationFrame(this.#raf);\n }\n\n #press(): void {\n const d = this.data;\n if (!d || d.canCompact !== true) return;\n const st = d.compactState ?? 'idle';\n if (st === 'sending' || st === 'sent' || st === 'queued') return;\n if (this.#armedAt && Date.now() - this.#armedAt < ARM_MS) {\n this.#disarm(false);\n this.emit('ui-compact', {});\n this.requestRender();\n return;\n }\n this.#armedAt = Date.now();\n clearTimeout(this.#armTimer);\n this.#armTimer = window.setTimeout(() => this.#disarm(), ARM_MS);\n this.requestRender();\n }\n\n #disarm(render = true): void {\n this.#armedAt = 0;\n clearTimeout(this.#armTimer);\n if (render) this.requestRender();\n }\n\n protected render(): string {\n const d = this.data;\n const t = this.#t;\n const win = num(d?.window);\n if (!d || !(win > 0)) return '';\n const parts = contextParts(d, t);\n const used = parts.reduce((s, p) => s + p.tokens, 0);\n const auto = num(d.autoCompactAt);\n const level = contextLevel(used, win, auto, d.compactPending);\n const state = d.compactState ?? 'idle';\n const busy = state === 'sending' || state === 'sent' || state === 'queued';\n const map = cellMap(parts, win);\n const mark = markIndex(auto, win);\n let cells = '';\n for (let n = 0; n < CELLS; n++) {\n const p = map[n] >= 0 ? parts[map[n]] : null;\n cells += `<i class=\"cw-cell\" style=\"--i:${n}${p ? `;--c:${p.color}` : ''}\"${p ? ` data-part=\"${p.key}\"` : ''}${\n mark >= 0 && n >= mark ? ' data-past' : ''\n }${p && busy ? ' data-busy' : ''}></i>`;\n }\n let rows = '';\n for (const p of parts) {\n const share = (p.tokens / win) * 100;\n rows += `<li class=\"cw-part\" data-part=\"${p.key}\" data-tok=\"${p.tokens}\" data-of=\"${escAttr(\n t.share(p.label, share.toFixed(share < 10 ? 1 : 0)),\n )}\"><i class=\"cw-sw\" style=\"background:${p.color}\"></i><span class=\"cw-label\">${esc(p.label)}</span><span class=\"cw-val\">${fmtTokens(p.tokens)}</span></li>`;\n }\n if (mark >= 0)\n rows += `<li class=\"cw-mark\"><i class=\"cw-sw is-mark\"></i><span class=\"cw-label\">${esc(t.mark)}</span><span class=\"cw-val\">${short(auto)}</span></li>`;\n const pct = `${Math.round(Math.min((used / win) * 100, 100))}%`;\n const ofTotal = t.of(short(win));\n const say = this.#say(d, used, auto, win, level, state, t);\n return `<section class=\"cw\" role=\"group\" aria-labelledby=\"cw-title\" data-level=\"${level}\">\n<div class=\"cw-head\"><span class=\"cw-title\" id=\"cw-title\">${esc(t.title)}</span>${d.model ? `<span class=\"cw-model\">${esc(d.model)}</span>` : ''}</div>\n<div class=\"cw-figure\"><span class=\"cw-big\" aria-hidden=\"true\" data-to=\"${used}\">${fmtTokens(this.#shown >= 0 ? this.#shown : used)}</span><span class=\"cw-of\" aria-hidden=\"true\" data-total=\"${escAttr(ofTotal)}\">${esc(ofTotal)}</span><span class=\"cw-pill\" aria-hidden=\"true\">${pct}</span><span class=\"sr-only\">${esc(t.sr(fmtTokens(used), short(win), pct, auto > 0 ? short(auto) : ''))}</span></div>\n<div class=\"cw-field\" aria-hidden=\"true\">${cells}</div>\n<ul class=\"cw-parts\">${rows}</ul>\n<div class=\"cw-foot\"><span class=\"cw-say\"><span class=\"cw-say-t${say.tone ? ` is-${say.tone}` : ''}\">${say.html}</span></span><span class=\"sr-only\" aria-live=\"polite\">${say.announce ? (say.sr ?? say.html) : ''}</span>${this.#action(d, state, used, t)}</div>\n</section>`;\n }\n\n #say(\n d: ContextWindowData,\n used: number,\n auto: number,\n win: number,\n level: ContextLevel,\n state: CompactState,\n t: Strings,\n ): { tone: string; html: string; sr?: string; announce?: boolean } {\n if (state === 'sending') return { tone: '', html: esc(t.sending), announce: true };\n if (state === 'sent') return { tone: '', html: esc(t.sent), announce: true };\n if (state === 'queued') return { tone: '', html: esc(t.queued), announce: true };\n if (this.#armedAt) return { tone: 'warn', html: esc(t.armed), sr: esc(t.armedSr), announce: true };\n if (state === 'failed') return { tone: 'fail', html: esc(t.failed), announce: true };\n const lc = d.lastCompact;\n if (state === 'observed') {\n if (lc && num(lc.preTokens) > num(lc.postTokens))\n return {\n tone: 'ok',\n html: t.saved(\n short(num(lc.preTokens) - num(lc.postTokens)),\n short(num(lc.preTokens)),\n short(num(lc.postTokens)),\n ),\n announce: true,\n };\n return { tone: 'ok', html: esc(t.observed), announce: true };\n }\n if (d.compactPending) return { tone: 'full', html: esc(t.pending), announce: true };\n const tone = level === 'full' ? 'full' : level === 'warn' ? 'warn' : '';\n if (auto > 0) {\n if (used >= auto) return { tone: 'full', html: esc(t.over) };\n if (level === 'full') return { tone: 'full', html: esc(t.soon(short(auto - used))) };\n return { tone, html: esc(t.left(short(auto - used))) };\n }\n return { tone, html: esc(t.free(short(Math.max(0, win - used)))) };\n }\n\n #action(d: ContextWindowData, state: CompactState, used: number, t: Strings): string {\n if (d.canCompact === undefined || d.canCompact === false || !(used > 0)) return '';\n const armed = !!this.#armedAt;\n const st =\n state === 'sending' || state === 'sent' || state === 'queued'\n ? 'busy'\n : state === 'observed'\n ? 'done'\n : armed\n ? 'armed'\n : 'idle';\n const label =\n state === 'sending'\n ? t.act.sending\n : state === 'sent' || state === 'queued'\n ? t.act.sent\n : st === 'done'\n ? t.act.done\n : st === 'armed'\n ? t.act.armed\n : t.act.idle;\n const icon =\n st === 'busy' ? '<span class=\"cw-spin\" aria-hidden=\"true\"></span>' : st === 'done' ? CHECK_SVG : COMPACT_SVG;\n const why = typeof d.canCompact === 'string' ? d.canCompact : '';\n const off = st === 'busy' || st === 'done' || !!why;\n return `<button type=\"button\" class=\"cw-compact\" data-key=\"cw-compact\" data-state=\"${st}\"${off ? ' disabled' : ''}${\n st === 'busy' ? ' aria-busy=\"true\"' : ''\n }${why ? ` title=\"${escAttr(why)}\"` : ''} aria-label=\"${escAttr(t.actLabel(label))}\"><span class=\"cw-ci\">${icon}</span><span class=\"cw-ct\">${esc(label)}</span></button>`;\n }\n\n protected override updated(): void {\n this.#applyHot();\n }\n\n /** Hover lives outside the markup: the hot part's cells lift, its row lights, the figure rolls to the part. */\n #applyHot(): void {\n const cw = this.$('.cw');\n if (!cw) return;\n let key = this.#hot;\n const row = key ? cw.querySelector(`.cw-part[data-part=\"${key}\"]`) : null;\n if (!row) key = '';\n cw.querySelector('.cw-field')?.toggleAttribute('data-hot', !!key);\n for (const c of Array.from(cw.querySelectorAll('.cw-cell[data-part]')))\n c.toggleAttribute('data-on', !!key && c.getAttribute('data-part') === key);\n for (const r of Array.from(cw.querySelectorAll('.cw-part, .cw-mark'))) {\n const mine = !!key && r.getAttribute('data-part') === key;\n r.toggleAttribute('data-hot', mine);\n r.toggleAttribute('data-dim', !!key && !mine);\n }\n const of = cw.querySelector('.cw-of');\n if (of) of.textContent = key ? (row?.getAttribute('data-of') ?? '') : (of.getAttribute('data-total') ?? '');\n const big = cw.querySelector<HTMLElement>('.cw-big');\n if (big) this.#roll(big, num(key ? row?.getAttribute('data-tok') : big.getAttribute('data-to')));\n }\n\n /** The big number rolls to its target (640ms, ease-out quart); first paint and reduced motion jump. */\n #roll(el: HTMLElement, to: number): void {\n cancelAnimationFrame(this.#raf);\n const from = this.#shown >= 0 ? this.#shown : to;\n if (from === to || prefersReducedMotion() || typeof requestAnimationFrame !== 'function') {\n this.#shown = to;\n el.textContent = fmtTokens(to);\n return;\n }\n const t0 = performance.now();\n const step = (now: number) => {\n const p = Math.min(1, (now - t0) / ROLL_MS);\n this.#shown = from + (to - from) * (1 - (1 - p) ** 4);\n el.textContent = fmtTokens(this.#shown);\n this.#raf = p < 1 ? requestAnimationFrame(step) : 0;\n };\n this.#raf = requestAnimationFrame(step);\n }\n}\n\ndefine('ui-context-window', UiContextWindow);\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ui-context-window': UiContextWindow;\n }\n}\n"],"names":["window"],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Base class for every @2lab.ai/ui component.
3
+ *
4
+ * - Open shadow root with `baseCss` + the subclass's `static styles`.
5
+ * - Data in, events out: set `el.data = {...}` (or the component's own properties) → one batched render;
6
+ * user intent leaves as a bubbling, composed `CustomEvent` (`ui-*`) that the host app handles.
7
+ * - `render()` returns an HTML string; every dynamic value must go through `esc` / `escAttr` / `safeHref`.
8
+ * - Focus survives a re-render when the focused element carries `data-key` (restored to the same key).
9
+ * - Subclasses that tick (timers) should update text nodes in place instead of re-rendering.
10
+ */
11
+ export declare abstract class UiElement<D = unknown> extends HTMLElement {
12
+ #private;
13
+ static styles: string[];
14
+ protected readonly root: ShadowRoot;
15
+ protected readonly content: HTMLElement;
16
+ constructor();
17
+ get data(): D | undefined;
18
+ set data(value: D | undefined);
19
+ connectedCallback(): void;
20
+ disconnectedCallback(): void;
21
+ /** Batch renders into one microtask. */
22
+ requestRender(): void;
23
+ /** Render synchronously (tests, or right before measuring). Cancels a queued render. */
24
+ renderNow(): void;
25
+ protected update(): void;
26
+ /** Where focus goes when the focused element is gone after a render (its key no longer rendered). Default: nowhere. */
27
+ protected focusFallback(): HTMLElement | null;
28
+ /** The component's markup. */
29
+ protected abstract render(): string;
30
+ /** Hook after each render (wire listeners on fresh nodes, start timers). */
31
+ protected updated(): void;
32
+ /** Dispatch a bubbling, composed, cancelable event. Returns false when a listener called preventDefault(). */
33
+ protected emit<T>(name: string, detail: T): boolean;
34
+ /** Query inside the shadow root. */
35
+ protected $<E extends Element = HTMLElement>(selector: string): E | null;
36
+ protected $$<E extends Element = HTMLElement>(selector: string): E[];
37
+ }
38
+ /** Register a custom element once (safe to import twice). */
39
+ export declare function define(tag: string, ctor: CustomElementConstructor): void;
40
+ /** True when the user asked for less motion. */
41
+ export declare function prefersReducedMotion(): boolean;
@@ -0,0 +1,14 @@
1
+ /** Token counts the way the dashboard shows them: 950, 12.3K, 272.8K, 1M, 1.2M. */
2
+ export declare function fmtTokens(n: number): string;
3
+ /** Grouped integer: 1,234,567. */
4
+ export declare function fmtInt(n: number): string;
5
+ /** Elapsed time: 42s, 9m 22s, 2h 15m. */
6
+ export declare function fmtDuration(ms: number): string;
7
+ /** Countdown / clock style m:ss (3:41). */
8
+ export declare function fmtClock(ms: number): string;
9
+ /** US dollars with 2 decimals. */
10
+ export declare function fmtUsd(n: number): string;
11
+ /** Relative past time: just now, 5m ago, 3h ago, 2d ago. */
12
+ export declare function fmtAgo(at: number, now?: number): string;
13
+ /** Clamp to [lo, hi]. */
14
+ export declare function clamp(n: number, lo: number, hi: number): number;
@@ -0,0 +1,10 @@
1
+ /** HTML-escape text for element content. Every model-, user- or tool-written string goes through this. */
2
+ export declare function esc(value: unknown): string;
3
+ /** Escape for a double-quoted attribute value. */
4
+ export declare function escAttr(value: unknown): string;
5
+ /** A link target that cannot run script: http(s)/mailto only, otherwise `#`. */
6
+ export declare function safeHref(url: unknown): string;
7
+ /** Join class names, skipping falsy entries. */
8
+ export declare function cls(...names: Array<string | false | null | undefined>): string;
9
+ /** `key="value"` pairs for truthy values, escaped. `true` renders a bare boolean attribute. */
10
+ export declare function attrs(map: Record<string, string | number | boolean | null | undefined>): string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Design tokens. Inside every component's shadow root the dashboard names (`--text`, `--surface`, `--st-run` …)
3
+ * are defined on `:host` and read the public `--ui-*` property first, so a page themes every component by
4
+ * setting `--ui-text` etc. (or loading `@2lab.ai/ui/tokens.css`). Without any page tokens a component follows
5
+ * `prefers-color-scheme`, and the `theme="light|dark"` attribute pins it.
6
+ */
7
+ export type TokenSet = Record<string, string>;
8
+ export declare const LIGHT: TokenSet;
9
+ export declare const DARK: TokenSet;
10
+ export declare const SHARED: TokenSet;
11
+ /** Shared stylesheet prepended to every component's shadow root. */
12
+ export declare const baseCss: string;
13
+ /** Optional page-level tokens (`@2lab.ai/ui/tokens.css`): `data-theme="dark|light"` on any ancestor switches. */
14
+ export declare const tokensCss: string;
@@ -0,0 +1,51 @@
1
+ function fmtTokens(n) {
2
+ const v = Number(n) || 0;
3
+ const a = Math.abs(v);
4
+ if (a >= 1e6) return `${trim(v / 1e6)}M`;
5
+ if (a >= 1e3) return `${trim(v / 1e3)}K`;
6
+ return String(Math.round(v));
7
+ }
8
+ function trim(x) {
9
+ const s = x.toFixed(1);
10
+ return s.endsWith(".0") ? s.slice(0, -2) : s;
11
+ }
12
+ function fmtInt(n) {
13
+ return Math.round(Number(n) || 0).toLocaleString("en-US");
14
+ }
15
+ function fmtDuration(ms) {
16
+ const s = Math.max(0, Math.floor((Number(ms) || 0) / 1e3));
17
+ if (s < 60) return `${s}s`;
18
+ const m = Math.floor(s / 60);
19
+ if (m < 60) return `${m}m ${String(s % 60).padStart(2, "0")}s`;
20
+ const h = Math.floor(m / 60);
21
+ return `${h}h ${String(m % 60).padStart(2, "0")}m`;
22
+ }
23
+ function fmtClock(ms) {
24
+ const s = Math.max(0, Math.ceil((Number(ms) || 0) / 1e3));
25
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
26
+ }
27
+ function fmtUsd(n) {
28
+ return `$${(Number(n) || 0).toFixed(2)}`;
29
+ }
30
+ function fmtAgo(at, now = Date.now()) {
31
+ const s = Math.max(0, Math.floor((now - at) / 1e3));
32
+ if (s < 45) return "just now";
33
+ const m = Math.floor(s / 60);
34
+ if (m < 60) return `${Math.max(1, m)}m ago`;
35
+ const h = Math.floor(m / 60);
36
+ if (h < 48) return `${h}h ago`;
37
+ return `${Math.floor(h / 24)}d ago`;
38
+ }
39
+ function clamp(n, lo, hi) {
40
+ return Math.min(hi, Math.max(lo, n));
41
+ }
42
+ export {
43
+ fmtClock as a,
44
+ fmtDuration as b,
45
+ clamp as c,
46
+ fmtInt as d,
47
+ fmtTokens as e,
48
+ fmtAgo as f,
49
+ fmtUsd as g
50
+ };
51
+ //# sourceMappingURL=format-BBGWXPzk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-BBGWXPzk.js","sources":["../src/core/format.ts"],"sourcesContent":["/** Token counts the way the dashboard shows them: 950, 12.3K, 272.8K, 1M, 1.2M. */\nexport function fmtTokens(n: number): string {\n const v = Number(n) || 0;\n const a = Math.abs(v);\n if (a >= 1_000_000) return `${trim(v / 1_000_000)}M`;\n if (a >= 1_000) return `${trim(v / 1_000)}K`;\n return String(Math.round(v));\n}\n\nfunction trim(x: number): string {\n const s = x.toFixed(1);\n return s.endsWith('.0') ? s.slice(0, -2) : s;\n}\n\n/** Grouped integer: 1,234,567. */\nexport function fmtInt(n: number): string {\n return Math.round(Number(n) || 0).toLocaleString('en-US');\n}\n\n/** Elapsed time: 42s, 9m 22s, 2h 15m. */\nexport function fmtDuration(ms: number): string {\n const s = Math.max(0, Math.floor((Number(ms) || 0) / 1000));\n if (s < 60) return `${s}s`;\n const m = Math.floor(s / 60);\n if (m < 60) return `${m}m ${String(s % 60).padStart(2, '0')}s`;\n const h = Math.floor(m / 60);\n return `${h}h ${String(m % 60).padStart(2, '0')}m`;\n}\n\n/** Countdown / clock style m:ss (3:41). */\nexport function fmtClock(ms: number): string {\n const s = Math.max(0, Math.ceil((Number(ms) || 0) / 1000));\n return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;\n}\n\n/** US dollars with 2 decimals. */\nexport function fmtUsd(n: number): string {\n return `$${(Number(n) || 0).toFixed(2)}`;\n}\n\n/** Relative past time: just now, 5m ago, 3h ago, 2d ago. */\nexport function fmtAgo(at: number, now: number = Date.now()): string {\n const s = Math.max(0, Math.floor((now - at) / 1000));\n if (s < 45) return 'just now';\n const m = Math.floor(s / 60);\n if (m < 60) return `${Math.max(1, m)}m ago`;\n const h = Math.floor(m / 60);\n if (h < 48) return `${h}h ago`;\n return `${Math.floor(h / 24)}d ago`;\n}\n\n/** Clamp to [lo, hi]. */\nexport function clamp(n: number, lo: number, hi: number): number {\n return Math.min(hi, Math.max(lo, n));\n}\n"],"names":[],"mappings":"AACO,SAAS,UAAU,GAAmB;AAC3C,QAAM,IAAI,OAAO,CAAC,KAAK;AACvB,QAAM,IAAI,KAAK,IAAI,CAAC;AACpB,MAAI,KAAK,IAAW,QAAO,GAAG,KAAK,IAAI,GAAS,CAAC;AACjD,MAAI,KAAK,IAAO,QAAO,GAAG,KAAK,IAAI,GAAK,CAAC;AACzC,SAAO,OAAO,KAAK,MAAM,CAAC,CAAC;AAC7B;AAEA,SAAS,KAAK,GAAmB;AAC/B,QAAM,IAAI,EAAE,QAAQ,CAAC;AACrB,SAAO,EAAE,SAAS,IAAI,IAAI,EAAE,MAAM,GAAG,EAAE,IAAI;AAC7C;AAGO,SAAS,OAAO,GAAmB;AACxC,SAAO,KAAK,MAAM,OAAO,CAAC,KAAK,CAAC,EAAE,eAAe,OAAO;AAC1D;AAGO,SAAS,YAAY,IAAoB;AAC9C,QAAM,IAAI,KAAK,IAAI,GAAG,KAAK,OAAO,OAAO,EAAE,KAAK,KAAK,GAAI,CAAC;AAC1D,MAAI,IAAI,GAAI,QAAO,GAAG,CAAC;AACvB,QAAM,IAAI,KAAK,MAAM,IAAI,EAAE;AAC3B,MAAI,IAAI,GAAI,QAAO,GAAG,CAAC,KAAK,OAAO,IAAI,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AAC3D,QAAM,IAAI,KAAK,MAAM,IAAI,EAAE;AAC3B,SAAO,GAAG,CAAC,KAAK,OAAO,IAAI,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AACjD;AAGO,SAAS,SAAS,IAAoB;AAC3C,QAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,EAAE,KAAK,KAAK,GAAI,CAAC;AACzD,SAAO,GAAG,KAAK,MAAM,IAAI,EAAE,CAAC,IAAI,OAAO,IAAI,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AACjE;AAGO,SAAS,OAAO,GAAmB;AACxC,SAAO,KAAK,OAAO,CAAC,KAAK,GAAG,QAAQ,CAAC,CAAC;AACxC;AAGO,SAAS,OAAO,IAAY,MAAc,KAAK,OAAe;AACnE,QAAM,IAAI,KAAK,IAAI,GAAG,KAAK,OAAO,MAAM,MAAM,GAAI,CAAC;AACnD,MAAI,IAAI,GAAI,QAAO;AACnB,QAAM,IAAI,KAAK,MAAM,IAAI,EAAE;AAC3B,MAAI,IAAI,GAAI,QAAO,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;AACpC,QAAM,IAAI,KAAK,MAAM,IAAI,EAAE;AAC3B,MAAI,IAAI,GAAI,QAAO,GAAG,CAAC;AACvB,SAAO,GAAG,KAAK,MAAM,IAAI,EAAE,CAAC;AAC9B;AAGO,SAAS,MAAM,GAAW,IAAY,IAAoB;AAC/D,SAAO,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC;AACrC;"}