@cardstack/boxel-cli 0.11.0-unstable.2 → 0.11.0-unstable.3

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 (106) hide show
  1. package/bundled-realms/pretui/components/agent-mark.gts +98 -0
  2. package/bundled-realms/pretui/components/agent-question.gts +340 -37
  3. package/bundled-realms/pretui/components/answer-footer.gts +249 -0
  4. package/bundled-realms/pretui/components/card-embed.gts +523 -0
  5. package/bundled-realms/pretui/components/card-pills.gts +725 -0
  6. package/bundled-realms/pretui/components/code-block.gts +87 -45
  7. package/bundled-realms/pretui/components/collapsed-turn.gts +133 -12
  8. package/bundled-realms/pretui/components/compaction-chip.gts +81 -67
  9. package/bundled-realms/pretui/components/composer-cap.gts +516 -0
  10. package/bundled-realms/pretui/components/context-ring.gts +108 -0
  11. package/bundled-realms/pretui/components/diff-block.gts +1002 -31
  12. package/bundled-realms/pretui/components/diff-stat.gts +102 -0
  13. package/bundled-realms/pretui/components/jump-chip.gts +86 -53
  14. package/bundled-realms/pretui/components/markdown-stream.gts +799 -0
  15. package/bundled-realms/pretui/components/markdown-surface.gts +71 -0
  16. package/bundled-realms/pretui/components/plan-mark.gts +147 -0
  17. package/bundled-realms/pretui/components/prose.gts +185 -8
  18. package/bundled-realms/pretui/components/prose.md +6 -8
  19. package/bundled-realms/pretui/components/quick-look.gts +377 -0
  20. package/bundled-realms/pretui/components/reactions.gts +136 -0
  21. package/bundled-realms/pretui/components/recommendation.gts +67 -36
  22. package/bundled-realms/pretui/components/result-card.gts +58 -22
  23. package/bundled-realms/pretui/components/segmented-control.gts +13 -8
  24. package/bundled-realms/pretui/components/session-prep.gts +12 -11
  25. package/bundled-realms/pretui/components/sliding-highlight.gts +8 -3
  26. package/bundled-realms/pretui/components/speaker-label.gts +132 -0
  27. package/bundled-realms/pretui/components/state-glyph.gts +121 -0
  28. package/bundled-realms/pretui/components/step-list.gts +62 -60
  29. package/bundled-realms/pretui/components/stick-to-bottom.gts +1150 -0
  30. package/bundled-realms/pretui/components/system-line.gts +143 -0
  31. package/bundled-realms/pretui/components/task-checklist.gts +184 -0
  32. package/bundled-realms/pretui/components/thinking.gts +63 -94
  33. package/bundled-realms/pretui/components/tool-change-table.gts +285 -0
  34. package/bundled-realms/pretui/components/tool-gate.gts +462 -0
  35. package/bundled-realms/pretui/components/tool-group.gts +920 -0
  36. package/bundled-realms/pretui/components/tool-icon.ts +64 -0
  37. package/bundled-realms/pretui/components/tool-meta.gts +82 -0
  38. package/bundled-realms/pretui/components/tool-plan.gts +197 -0
  39. package/bundled-realms/pretui/components/tool-row.gts +2087 -0
  40. package/bundled-realms/pretui/components/turn-changes.gts +227 -0
  41. package/bundled-realms/pretui/components/user-bubble.gts +575 -0
  42. package/bundled-realms/pretui/components/work-item.gts +185 -48
  43. package/bundled-realms/pretui/docs/scroll-anchoring-protocol.md +136 -0
  44. package/bundled-realms/pretui/internal/markdown-projection.ts +438 -0
  45. package/bundled-realms/pretui/internal/mount-queue.ts +79 -0
  46. package/bundled-realms/pretui/internal/tool-phrases.ts +168 -0
  47. package/bundled-realms/pretui/package.json +2 -1
  48. package/bundled-test-harness/assets/{ai-module-creation-test-beRNwH-e.js → ai-module-creation-test-B6FQKqIl.js} +1 -1
  49. package/bundled-test-harness/assets/{app-CtezUCip.js → app-KpPqdyX3.js} +4 -4
  50. package/bundled-test-harness/assets/{binary-upload-dedupe-test-bI8T0QLl.js → binary-upload-dedupe-test-DN00y1tM.js} +1 -1
  51. package/bundled-test-harness/assets/{can-invoke-test-DVaU_yRT.js → can-invoke-test-Bl-EUFM3.js} +1 -1
  52. package/bundled-test-harness/assets/{card-chooser-paging-test-DnxMnXKR.js → card-chooser-paging-test-BNuZRHX3.js} +1 -1
  53. package/bundled-test-harness/assets/{card-chooser-test-DUSnLr-u.js → card-chooser-test-wKfPOOT4.js} +1 -1
  54. package/bundled-test-harness/assets/{card-def-field-def-relationships-test-GBUr3tFK.js → card-def-field-def-relationships-test-CduQ6zan.js} +1 -1
  55. package/bundled-test-harness/assets/{card-delete-test-CQveO1VC.js → card-delete-test-DKxolj5o.js} +1 -1
  56. package/bundled-test-harness/assets/{check-correctness-test-D2M2p66N.js → check-correctness-test-B-kgHztp.js} +1 -1
  57. package/bundled-test-harness/assets/{codeblocks-test-AdolhdCA.js → codeblocks-test-BDD7qK_V.js} +1 -1
  58. package/bundled-test-harness/assets/{compaction-test-DvyAQq56.js → compaction-test-BweolkhG.js} +1 -1
  59. package/bundled-test-harness/assets/{container-DIF_9lZ9.css → container-B5B6u-xm.css} +29 -22
  60. package/bundled-test-harness/assets/{container-DvvqmNpg.js → container-BnJjIM6J.js} +9 -6
  61. package/bundled-test-harness/assets/{create-listing-modal-test-D95JfQab.js → create-listing-modal-test-FvcAKpIa.js} +1 -1
  62. package/bundled-test-harness/assets/{debug-message-test-BcNtj7Wr.js → debug-message-test-CJ18Evt7.js} +1 -1
  63. package/bundled-test-harness/assets/{externals-DCnB4XSS.js → externals-BWjhmCD4.js} +1 -1
  64. package/bundled-test-harness/assets/{fallback-banner-live-delete-test-5lnN1pv2.js → fallback-banner-live-delete-test-C2H3BrT-.js} +1 -1
  65. package/bundled-test-harness/assets/{fallback-banner-test-DFb_m_Kq.js → fallback-banner-test-C10aYjkj.js} +1 -1
  66. package/bundled-test-harness/assets/{fallback-models-test-p3YIwj1L.js → fallback-models-test-BxoPiaGF.js} +1 -1
  67. package/bundled-test-harness/assets/{file-attachment-test-CmyjNWRR.js → file-attachment-test-DeKyzYaz.js} +1 -1
  68. package/bundled-test-harness/assets/{general-test-CX-TFDuX.js → general-test-BewzrpoB.js} +1 -1
  69. package/bundled-test-harness/assets/{index-query-engine-test-CY2tAkot.js → index-query-engine-test-BtI8Y2LR.js} +1 -1
  70. package/bundled-test-harness/assets/{instance-filter-matcher-test-CMBTh8xn.js → instance-filter-matcher-test-BHXHGHjE.js} +1 -1
  71. package/bundled-test-harness/assets/{main-BOZbZVem.js → main-CqAHZ2kK.js} +1 -1
  72. package/bundled-test-harness/assets/{message-tool-test-CfjRSJvt.js → message-tool-test-NMaPJFsd.js} +1 -1
  73. package/bundled-test-harness/assets/{model-cost-tier-test-mZG84Uso.js → model-cost-tier-test-BgbfN2LH.js} +1 -1
  74. package/bundled-test-harness/assets/{operation-lowering-test-CHnjKSJb.js → operation-lowering-test-C4IaOjsA.js} +1 -1
  75. package/bundled-test-harness/assets/{operator-mode-card-chooser-test-KWS8lq16.js → operator-mode-card-chooser-test-Bwe47Rn7.js} +1 -1
  76. package/bundled-test-harness/assets/{operator-mode-links-test-CViFt0Ws.js → operator-mode-links-test-BU4jkC-B.js} +1 -1
  77. package/bundled-test-harness/assets/{operator-mode-test-D0gZM2yw.js → operator-mode-test-Bh0rGM9z.js} +1 -1
  78. package/bundled-test-harness/assets/{operator-mode-ui-test-CEvHJ9DS.js → operator-mode-ui-test-whgUw0ju.js} +1 -1
  79. package/bundled-test-harness/assets/{overlay-menu-items-test-Pw80RXsr.js → overlay-menu-items-test-Cwn1vr8q.js} +1 -1
  80. package/bundled-test-harness/assets/{past-sessions-test-CW0-Bjav.js → past-sessions-test-Bh24N-Kz.js} +1 -1
  81. package/bundled-test-harness/assets/{permission-checking-realm-test-D5uXQypS.js → permission-checking-realm-test-DKnnAy0f.js} +1 -1
  82. package/bundled-test-harness/assets/{realm-config-policy-standing-test-Btatheni.js → realm-config-policy-standing-test-DngdYypq.js} +1 -1
  83. package/bundled-test-harness/assets/{realm-config-routing-rule-edit-test-Dh5zW13o.js → realm-config-routing-rule-edit-test-_rkFU8SN.js} +1 -1
  84. package/bundled-test-harness/assets/{realm-config-settings-test-BJa8gv_C.js → realm-config-settings-test-CR9NH0Zn.js} +1 -1
  85. package/bundled-test-harness/assets/{reasoning-test-2Zp37cQB.js → reasoning-test-0lH10kRl.js} +1 -1
  86. package/bundled-test-harness/assets/{room-message-test-BsrON13e.js → room-message-test-C-PddE4Y.js} +1 -1
  87. package/bundled-test-harness/assets/{run-realm-code-BxktQf6R.js → run-realm-code-C8N7DtN-.js} +1 -1
  88. package/bundled-test-harness/assets/{run-realm-code-test-7grpTos-.js → run-realm-code-test-DBdp147q.js} +1 -1
  89. package/bundled-test-harness/assets/{schema-generation-test-CM9ggQji.js → schema-generation-test-CB1Hb7GQ.js} +1 -1
  90. package/bundled-test-harness/assets/{scrolling-test-BER00htB.js → scrolling-test-3PhVin8h.js} +1 -1
  91. package/bundled-test-harness/assets/{searchable-option-test-Ctf8r3HZ.js → searchable-option-test-BAdOiL4U.js} +1 -1
  92. package/bundled-test-harness/assets/{sending-test-BJGobs2E.js → sending-test-KSmu1tSJ.js} +1 -1
  93. package/bundled-test-harness/assets/{skills-test-GPOsHRN6.js → skills-test-D-dlcFpd.js} +1 -1
  94. package/bundled-test-harness/assets/{store-test-QvvHimzH.js → store-test-CEW7xQjZ.js} +1 -1
  95. package/bundled-test-harness/assets/{tests-DoTEpvcF.js → tests-QYqqa6nv.js} +47 -47
  96. package/bundled-test-harness/assets/{text-input-validator-test-CYYH2N0L.js → text-input-validator-test-D8iNOWep.js} +1 -1
  97. package/bundled-test-harness/assets/{to-device-streaming-test-Ctqa0bVp.js → to-device-streaming-test-trCX-505.js} +1 -1
  98. package/bundled-test-harness/assets/{token-usage-test-BQVbK3X0.js → token-usage-test-C6sULooH.js} +1 -1
  99. package/bundled-test-harness/assets/{tools-DU8Rc15s.js → tools-UJfD-_al.js} +1 -1
  100. package/bundled-test-harness/assets/{tools-test-B13yvEKE.js → tools-test-Brav31AV.js} +1 -1
  101. package/bundled-test-harness/assets/{worker-DHBEBihX.js → worker-9ph9_Lpv.js} +31 -26
  102. package/bundled-test-harness/index.html +8 -8
  103. package/bundled-test-harness/tests/index.html +8 -8
  104. package/bundled-types/host-app/lib/realm-runner/worker.ts +8 -51
  105. package/bundled-types/runtime-common/realm-runner-script.d.ts +3 -0
  106. package/package.json +1 -1
@@ -0,0 +1,516 @@
1
+ // Pretui — ComposerCap: the dark cap on the composer's top edge. One status row, what the agent does now; the messages queued behind it; then the box.
2
+ import Component from '@glimmer/component';
3
+ import { hash } from '@ember/helper';
4
+ import { on } from '@ember/modifier';
5
+ import type { ComponentLike } from '@glint/template';
6
+ import ArrowDownIcon from '@cardstack/boxel-icons/arrow-down';
7
+ import ChevronDownIcon from '@cardstack/boxel-icons/chevron-down';
8
+ import ChevronUpIcon from '@cardstack/boxel-icons/chevron-up';
9
+ import ListIcon from '@cardstack/boxel-icons/list';
10
+ import PencilIcon from '@cardstack/boxel-icons/pencil';
11
+ import PlayerStopIcon from '@cardstack/boxel-icons/player-stop';
12
+ import RotateCcwIcon from '@cardstack/boxel-icons/rotate-ccw';
13
+ import XIcon from '@cardstack/boxel-icons/x';
14
+ import { AgentMark } from './agent-mark';
15
+
16
+ export type CapTone = 'working' | 'waiting' | 'problem' | 'done' | 'idle';
17
+ export type CapButtonKind = 'primary' | 'secondary' | 'quiet' | 'icon';
18
+ export type CapGlyph =
19
+ | 'stop'
20
+ | 'retry'
21
+ | 'jump'
22
+ | 'edit'
23
+ | 'remove'
24
+ | 'collapse'
25
+ | 'expand';
26
+
27
+ export interface CapButtonSignature {
28
+ Args: {
29
+ /** primary is the moment's one action, in solid --primary. Default 'secondary' */
30
+ kind?: CapButtonKind;
31
+ /** a glyph before the words, or alone for kind 'icon' */
32
+ glyph?: CapGlyph;
33
+ /** the click is running: the button holds its size and greys */
34
+ busy?: boolean;
35
+ };
36
+ Blocks: { default: [] };
37
+ Element: HTMLButtonElement;
38
+ }
39
+
40
+ /** A button sized for the cap's row: 1.75rem in the status row, 1.5rem in a queued row. */
41
+ export class CapButton extends Component<CapButtonSignature> {
42
+ <template>
43
+ <button
44
+ type='button'
45
+ class='pretui-cap-button'
46
+ data-kind={{if @kind @kind 'secondary'}}
47
+ data-busy={{if @busy 'true'}}
48
+ aria-busy={{if @busy 'true'}}
49
+ ...attributes
50
+ >{{#if @glyph}}<CapGlyphIcon @glyph={{@glyph}} />{{/if}}{{yield}}</button>
51
+ </template>
52
+ }
53
+
54
+ type CapIcon = ComponentLike<{ Element: SVGSVGElement }>;
55
+
56
+ /** each glyph is a boxel-icons mark, so the cap draws its icons the way
57
+ * the rest of the kit does and a theme recolours them all */
58
+ const CAP_ICONS: Record<CapGlyph, CapIcon> = {
59
+ stop: PlayerStopIcon as CapIcon,
60
+ retry: RotateCcwIcon as CapIcon,
61
+ jump: ArrowDownIcon as CapIcon,
62
+ edit: PencilIcon as CapIcon,
63
+ remove: XIcon as CapIcon,
64
+ collapse: ChevronUpIcon as CapIcon,
65
+ expand: ChevronDownIcon as CapIcon,
66
+ };
67
+
68
+ class CapGlyphIcon extends Component<{ Args: { glyph: CapGlyph } }> {
69
+ get icon(): CapIcon {
70
+ return CAP_ICONS[this.args.glyph];
71
+ }
72
+ <template>
73
+ <this.icon
74
+ class='pretui-cap-glyph'
75
+ data-glyph={{@glyph}}
76
+ width='12'
77
+ height='12'
78
+ aria-hidden='true'
79
+ />
80
+ </template>
81
+ }
82
+
83
+ export interface CapStatusSignature {
84
+ Args: {
85
+ /** what kind of moment: working animates the agent's mark; waiting is the
86
+ attention dot; problem the destructive dot; done the success dot;
87
+ idle the still mark, dimmed. Default 'working' */
88
+ tone?: CapTone;
89
+ /** the status in a few words: "Generating results", "Run 3 tool calls" */
90
+ title: string;
91
+ /** what qualifies it, quieter: "3 calls", "1 of 3" */
92
+ detail?: string;
93
+ /** three breathing dots after the title, for a reply still arriving */
94
+ dots?: boolean;
95
+ /** let the words wrap: an instruction ("You need a minimum of…") */
96
+ wrap?: boolean;
97
+ };
98
+ Blocks: { default: [] };
99
+ Element: HTMLDivElement;
100
+ }
101
+
102
+ /** The cap's one status row. Its actions are the block, right-aligned. */
103
+ export class CapStatus extends Component<CapStatusSignature> {
104
+ get tone(): CapTone {
105
+ return this.args.tone ?? 'working';
106
+ }
107
+ get showsMark() {
108
+ return this.tone === 'working' || this.tone === 'idle';
109
+ }
110
+ get working() {
111
+ return this.tone === 'working';
112
+ }
113
+ <template>
114
+ <div
115
+ class='pretui-cap-row pretui-cap-status'
116
+ data-tone={{this.tone}}
117
+ data-wrap={{if @wrap 'true'}}
118
+ data-test-pretui-cap-status={{this.tone}}
119
+ ...attributes
120
+ >
121
+ <span class='pretui-cap-lead' aria-hidden='true'>
122
+ {{#if this.showsMark}}
123
+ <AgentMark @working={{this.working}} @size={{18}} />
124
+ {{else}}
125
+ <span class='pretui-cap-dot'></span>
126
+ {{/if}}
127
+ </span>
128
+ {{! the status region holds the words only, so a change announces the
129
+ status and not the row's buttons }}
130
+ <span
131
+ class='pretui-cap-text'
132
+ role='status'
133
+ aria-atomic='true'
134
+ data-test-pretui-cap-status-text
135
+ ><span class='pretui-cap-title'>{{@title}}</span>{{#if @dots}}<span
136
+ class='pretui-cap-dots'
137
+ aria-hidden='true'
138
+ ><span>.</span><span>.</span><span>.</span></span>{{/if}}{{#if
139
+ @detail
140
+ }}<span class='pretui-cap-detail'> · {{@detail}}</span>{{/if}}</span>
141
+ <span class='pretui-cap-actions'>{{yield}}</span>
142
+ </div>
143
+ </template>
144
+ }
145
+
146
+ export interface CapQueuedSignature {
147
+ Args: {
148
+ /** where it stands in line: "Next", "Then" */
149
+ order?: string;
150
+ text: string;
151
+ /** pull it back into the box to change it */
152
+ onEdit?: () => void;
153
+ /** take it out of the queue */
154
+ onRemove?: () => void;
155
+ };
156
+ Element: HTMLDivElement;
157
+ }
158
+
159
+ /** A message waiting behind the running turn, in send order under the status. */
160
+ export class CapQueued extends Component<CapQueuedSignature> {
161
+ edit = () => this.args.onEdit?.();
162
+ remove = () => this.args.onRemove?.();
163
+ <template>
164
+ <div
165
+ class='pretui-cap-row pretui-cap-queued'
166
+ data-test-pretui-cap-queued
167
+ ...attributes
168
+ >
169
+ <span class='pretui-cap-lead' aria-hidden='true'><ListIcon
170
+ class='pretui-cap-glyph'
171
+ width='13'
172
+ height='13'
173
+ /></span>
174
+ <span class='pretui-cap-text'>{{#if @order}}<span
175
+ class='pretui-cap-detail'
176
+ >{{@order}} · </span>{{/if}}{{@text}}</span>
177
+ <span class='pretui-cap-actions'>
178
+ {{#if @onEdit}}
179
+ <CapButton
180
+ @kind='icon'
181
+ @glyph='edit'
182
+ aria-label='Edit queued message'
183
+ {{on 'click' this.edit}}
184
+ data-test-pretui-cap-queued-edit
185
+ />
186
+ {{/if}}
187
+ {{#if @onRemove}}
188
+ <CapButton
189
+ @kind='icon'
190
+ @glyph='remove'
191
+ aria-label='Remove queued message'
192
+ {{on 'click' this.remove}}
193
+ data-test-pretui-cap-queued-remove
194
+ />
195
+ {{/if}}
196
+ </span>
197
+ </div>
198
+ </template>
199
+ }
200
+
201
+ export interface CapSectionSignature {
202
+ Blocks: { default: [] };
203
+ Element: HTMLDivElement;
204
+ }
205
+
206
+ /** Room under the status for what belongs to it: a script's plan. */
207
+ const CapSection = <template>
208
+ <div class='pretui-cap-section' ...attributes>{{yield}}</div>
209
+ </template> satisfies import('@ember/component/template-only').TemplateOnlyComponent<CapSectionSignature>;
210
+
211
+ export interface ComposerCapSignature {
212
+ Args: {
213
+ /** the group's accessible name. Default 'Assistant status' */
214
+ label?: string;
215
+ };
216
+ Blocks: {
217
+ default: [
218
+ {
219
+ Status: typeof CapStatus;
220
+ Queued: typeof CapQueued;
221
+ Section: typeof CapSection;
222
+ Button: typeof CapButton;
223
+ },
224
+ ];
225
+ };
226
+ Element: HTMLDivElement;
227
+ }
228
+
229
+ /**
230
+ * The dark cap on the composer's top edge. Read down, the panel is a
231
+ * timeline: the transcript (done), the cap's status (now), its queue (next),
232
+ * the box (what you are still writing). One status at a time, the most
233
+ * urgent: a decision, a problem, work, idle.
234
+ */
235
+ export class ComposerCap extends Component<ComposerCapSignature> {
236
+ <template>
237
+ <div
238
+ class='pretui-cap'
239
+ role='group'
240
+ aria-label={{if @label @label 'Assistant status'}}
241
+ data-test-pretui-composer-cap
242
+ ...attributes
243
+ >
244
+ {{yield
245
+ (hash
246
+ Status=CapStatus Queued=CapQueued Section=CapSection Button=CapButton
247
+ )
248
+ }}
249
+ </div>
250
+ <style scoped>
251
+ @layer PretComponent {
252
+ .pretui-cap {
253
+ --pretui-cap-ink: var(--foreground);
254
+ /* a row's least height, which its buttons fill */
255
+ --_cap-row-h: 1.75rem;
256
+ display: flex;
257
+ flex-direction: column;
258
+ margin-inline: var(--pretui-cap-inset, var(--boxel-sp-sm));
259
+ border-radius: var(--boxel-border-radius-lg)
260
+ var(--boxel-border-radius-lg) 0 0;
261
+ background-color: var(
262
+ --pretui-cap-surface,
263
+ color-mix(in oklch, var(--foreground) 8%, var(--background))
264
+ );
265
+ box-shadow: 0 0 0 1px var(--pretui-cap-edge, var(--border-strong));
266
+ color: var(--pretui-cap-ink);
267
+ font-family: var(--font-sans);
268
+ container: pretui-cap / inline-size;
269
+ }
270
+ /* a narrow cap keeps the moment's primary and drops what it can
271
+ reach another way */
272
+ @container pretui-cap (width < 20rem) {
273
+ .pretui-cap :deep([data-cap-optional]) {
274
+ display: none;
275
+ }
276
+ }
277
+ /* every row is UI size; a queued row is the compact one */
278
+ .pretui-cap :deep(.pretui-cap-row) {
279
+ display: flex;
280
+ align-items: center;
281
+ gap: var(--boxel-sp-xs);
282
+ min-height: var(--_cap-row-h);
283
+ padding: 0 var(--boxel-sp-4xs) 0 var(--boxel-sp-xs);
284
+ font-size: var(--boxel-ui-label-font-size);
285
+ line-height: var(--boxel-ui-label-line-height);
286
+ font-weight: 400;
287
+ min-width: 0;
288
+ }
289
+ .pretui-cap > :deep(* + *) {
290
+ box-shadow: inset 0 1px 0 var(--pretui-cap-edge, var(--border-strong));
291
+ }
292
+ /* the status is the row you read first: one step stronger than the
293
+ queue by weight (its title is 600) and height, never by size */
294
+ .pretui-cap :deep(.pretui-cap-status) {
295
+ min-height: 2.25rem;
296
+ padding: 0 var(--boxel-sp-4xs) 0 var(--boxel-sp-xs);
297
+ }
298
+ .pretui-cap :deep(.pretui-cap-status[data-wrap]) {
299
+ align-items: flex-start;
300
+ padding-block: var(--boxel-sp-xs);
301
+ }
302
+ .pretui-cap :deep(.pretui-cap-status[data-wrap] .pretui-cap-lead) {
303
+ margin-top: 0;
304
+ }
305
+ .pretui-cap :deep(.pretui-cap-status[data-wrap] .pretui-cap-actions) {
306
+ margin-top: calc(-1 * var(--boxel-sp-4xs));
307
+ }
308
+ .pretui-cap :deep(.pretui-cap-lead) {
309
+ flex: none;
310
+ display: inline-flex;
311
+ align-items: center;
312
+ justify-content: center;
313
+ width: 1.125rem;
314
+ height: 1.125rem;
315
+ }
316
+ .pretui-cap :deep(.pretui-cap-queued .pretui-cap-lead) {
317
+ width: 1rem;
318
+ height: 1rem;
319
+ color: var(--subtle-foreground);
320
+ }
321
+ .pretui-cap
322
+ :deep(.pretui-cap-status[data-tone='idle'] .pretui-cap-lead) {
323
+ opacity: 0.55;
324
+ }
325
+ .pretui-cap :deep(.pretui-cap-dot) {
326
+ width: 0.5rem;
327
+ height: 0.5rem;
328
+ border-radius: 50%;
329
+ background-color: currentColor;
330
+ box-shadow: 0 0 0 0.1875rem
331
+ color-mix(in oklch, currentColor 20%, transparent);
332
+ transition: color 160ms ease;
333
+ }
334
+ /* a state colours the mark only: waiting on you is attention, a
335
+ problem is failed, done and live work are live */
336
+ .pretui-cap
337
+ :deep(.pretui-cap-status[data-tone='waiting'] .pretui-cap-lead) {
338
+ color: var(--attention-ink);
339
+ }
340
+ .pretui-cap
341
+ :deep(.pretui-cap-status[data-tone='problem'] .pretui-cap-lead) {
342
+ color: var(--destructive-ink);
343
+ }
344
+ .pretui-cap
345
+ :deep(.pretui-cap-status[data-tone='done'] .pretui-cap-lead) {
346
+ color: var(--primary-ink);
347
+ }
348
+ /* the title holds its words; the detail gives way first */
349
+ .pretui-cap :deep(.pretui-cap-text) {
350
+ flex: 1 1 auto;
351
+ display: flex;
352
+ align-items: baseline;
353
+ min-width: 0;
354
+ overflow: hidden;
355
+ white-space: nowrap;
356
+ }
357
+ .pretui-cap :deep(.pretui-cap-title) {
358
+ flex: 0 1 auto;
359
+ min-width: 0;
360
+ overflow: hidden;
361
+ text-overflow: ellipsis;
362
+ }
363
+ .pretui-cap :deep(.pretui-cap-dots) {
364
+ flex: none;
365
+ }
366
+ .pretui-cap :deep(.pretui-cap-text > .pretui-cap-detail) {
367
+ flex: 0 4 auto;
368
+ min-width: 0;
369
+ overflow: hidden;
370
+ text-overflow: ellipsis;
371
+ white-space: pre;
372
+ }
373
+ .pretui-cap :deep(.pretui-cap-status[data-wrap] .pretui-cap-text) {
374
+ display: block;
375
+ white-space: normal;
376
+ overflow-wrap: anywhere;
377
+ color: var(--muted-foreground);
378
+ }
379
+ .pretui-cap :deep(.pretui-cap-status[data-wrap] .pretui-cap-title) {
380
+ font-weight: 400;
381
+ }
382
+ .pretui-cap :deep(.pretui-cap-title) {
383
+ font-weight: 600;
384
+ }
385
+ .pretui-cap :deep(.pretui-cap-queued .pretui-cap-text) {
386
+ color: var(--muted-foreground);
387
+ }
388
+ .pretui-cap :deep(.pretui-cap-detail) {
389
+ color: var(--muted-foreground);
390
+ font-weight: 400;
391
+ font-variant-numeric: tabular-nums;
392
+ }
393
+ .pretui-cap :deep(.pretui-cap-dots span) {
394
+ animation: pretui-cap-blink 1.4s infinite both;
395
+ opacity: 0.4;
396
+ }
397
+ .pretui-cap :deep(.pretui-cap-dots span:nth-child(2)) {
398
+ animation-delay: 0.2s;
399
+ }
400
+ .pretui-cap :deep(.pretui-cap-dots span:nth-child(3)) {
401
+ animation-delay: 0.4s;
402
+ }
403
+ @keyframes pretui-cap-blink {
404
+ 0%,
405
+ 80%,
406
+ 100% {
407
+ opacity: 0.4;
408
+ }
409
+ 40% {
410
+ opacity: 1;
411
+ }
412
+ }
413
+ .pretui-cap :deep(.pretui-cap-actions) {
414
+ flex: none;
415
+ display: flex;
416
+ align-items: center;
417
+ gap: var(--boxel-sp-4xs);
418
+ }
419
+ .pretui-cap :deep(.pretui-cap-actions:empty) {
420
+ display: none;
421
+ }
422
+ .pretui-cap :deep(.pretui-cap-section) {
423
+ padding: var(--boxel-sp-4xs) var(--boxel-sp-sm) var(--boxel-sp-xs)
424
+ var(--boxel-sp-xl);
425
+ }
426
+ .pretui-cap :deep(.pretui-cap-button) {
427
+ display: inline-flex;
428
+ align-items: center;
429
+ justify-content: center;
430
+ gap: var(--boxel-sp-4xs);
431
+ height: 1.5rem;
432
+ min-width: 1.5rem;
433
+ padding: 0 var(--boxel-sp-xs);
434
+ border: 0;
435
+ border-radius: var(--boxel-border-radius-sm);
436
+ background-color: transparent;
437
+ color: var(--muted-foreground);
438
+ font-family: var(--font-sans);
439
+ font-size: var(--boxel-ui-label-font-size);
440
+ line-height: var(--boxel-ui-label-line-height);
441
+ font-weight: 600;
442
+ white-space: nowrap;
443
+ cursor: pointer;
444
+ }
445
+ .pretui-cap :deep(.pretui-cap-status .pretui-cap-button) {
446
+ height: var(--_cap-row-h);
447
+ min-width: var(--_cap-row-h);
448
+ padding: 0 var(--boxel-sp-sm);
449
+ }
450
+ .pretui-cap
451
+ :deep(.pretui-cap-status .pretui-cap-button[data-kind='icon']) {
452
+ padding: 0;
453
+ }
454
+ .pretui-cap :deep(.pretui-cap-button[data-kind='icon']) {
455
+ padding: 0;
456
+ }
457
+ /* the moment's one action */
458
+ .pretui-cap :deep(.pretui-cap-button[data-kind='primary']) {
459
+ background-color: var(--primary);
460
+ color: var(--primary-foreground);
461
+ }
462
+ .pretui-cap
463
+ :deep(.pretui-cap-button[data-kind='primary']:hover:not(:disabled)) {
464
+ background-color: color-mix(
465
+ in oklch,
466
+ var(--primary) 88%,
467
+ var(--pretui-cap-ink)
468
+ );
469
+ }
470
+ .pretui-cap :deep(.pretui-cap-button[data-kind='secondary']) {
471
+ box-shadow: inset 0 0 0 1px var(--border-strong);
472
+ color: var(--pretui-cap-ink);
473
+ }
474
+ .pretui-cap :deep(.pretui-cap-button[data-kind='quiet']),
475
+ .pretui-cap :deep(.pretui-cap-button[data-kind='icon']) {
476
+ color: var(--muted-foreground);
477
+ }
478
+ .pretui-cap
479
+ :deep(.pretui-cap-button[data-kind='quiet']:hover:not(:disabled)),
480
+ .pretui-cap
481
+ :deep(.pretui-cap-button[data-kind='icon']:hover:not(:disabled)) {
482
+ color: var(--pretui-cap-ink);
483
+ background-color: color-mix(
484
+ in oklch,
485
+ var(--pretui-cap-ink) 8%,
486
+ transparent
487
+ );
488
+ }
489
+ .pretui-cap :deep(.pretui-cap-button:disabled),
490
+ .pretui-cap :deep(.pretui-cap-button[data-busy]) {
491
+ cursor: default;
492
+ background-color: color-mix(
493
+ in oklch,
494
+ var(--pretui-cap-ink) 10%,
495
+ transparent
496
+ );
497
+ color: var(--subtle-foreground);
498
+ box-shadow: none;
499
+ }
500
+ .pretui-cap :deep(.pretui-cap-button:focus-visible) {
501
+ outline: 2px solid var(--ring);
502
+ outline-offset: 1px;
503
+ }
504
+ .pretui-cap :deep(.pretui-cap-glyph) {
505
+ flex: none;
506
+ }
507
+ @media (prefers-reduced-motion: reduce) {
508
+ .pretui-cap :deep(.pretui-cap-dots span) {
509
+ animation: none;
510
+ opacity: 0.7;
511
+ }
512
+ }
513
+ }
514
+ </style>
515
+ </template>
516
+ }
@@ -0,0 +1,108 @@
1
+ // Pretui — ContextRing: how much of the model's context the conversation uses, as a small ring.
2
+ import Component from '@glimmer/component';
3
+ import { htmlSafe } from '@ember/template';
4
+
5
+ export interface ContextRingSignature {
6
+ Args: {
7
+ /** tokens the conversation uses */
8
+ used: number;
9
+ /** the model's context window, in tokens */
10
+ limit: number;
11
+ /** the share at which the ring warns. Default 0.8 */
12
+ warnAt?: number;
13
+ /** the ring alone: the percentage moves to its tooltip, for a tight
14
+ * composer bar */
15
+ compact?: boolean;
16
+ };
17
+ Element: HTMLSpanElement;
18
+ }
19
+
20
+ function compact(n: number): string {
21
+ return n >= 1000 ? `${Math.round(n / 100) / 10}k` : String(n);
22
+ }
23
+
24
+ /**
25
+ * A small ring and its percentage, beside the model picker: how full the
26
+ * context is, so a reader sees compaction coming before it happens. The
27
+ * ring turns to the attention colour past @warnAt, and its name says
28
+ * "nearly full".
29
+ */
30
+ export class ContextRing extends Component<ContextRingSignature> {
31
+ get share(): number {
32
+ let { used, limit } = this.args;
33
+ return limit > 0 ? Math.min(Math.max(used / limit, 0), 1) : 0;
34
+ }
35
+ get percent(): number {
36
+ return Math.round(this.share * 100);
37
+ }
38
+ get warns(): boolean {
39
+ return this.share >= (this.args.warnAt ?? 0.8);
40
+ }
41
+ get ringStyle() {
42
+ return htmlSafe(`--pretui-ring-share: ${this.share * 360}deg`);
43
+ }
44
+ /** the accessible name and tooltip; past @warnAt it says so in words, so
45
+ * the warning never rests on the ring's colour alone */
46
+ get label(): string {
47
+ let warning = this.warns ? ', nearly full' : '';
48
+ return `Context ${this.percent}% used${warning}: ${compact(this.args.used)} of ${compact(this.args.limit)} tokens`;
49
+ }
50
+ <template>
51
+ <span
52
+ class='pretui-contextring'
53
+ role='img'
54
+ aria-label={{this.label}}
55
+ title={{this.label}}
56
+ data-warn={{if this.warns 'true'}}
57
+ data-test-pretui-context-ring
58
+ ...attributes
59
+ >
60
+ <span
61
+ class='pretui-contextring-ring'
62
+ style={{this.ringStyle}}
63
+ aria-hidden='true'
64
+ ></span>
65
+ {{#unless @compact}}<span
66
+ class='pretui-contextring-pct'
67
+ aria-hidden='true'
68
+ >{{this.percent}}%</span>{{/unless}}
69
+ </span>
70
+ <style scoped>
71
+ @layer PretComponent {
72
+ .pretui-contextring {
73
+ display: inline-flex;
74
+ align-items: center;
75
+ gap: var(--boxel-sp-4xs);
76
+ font-size: var(--boxel-caption-font-size);
77
+ line-height: var(--boxel-caption-line-height);
78
+ font-variant-numeric: tabular-nums;
79
+ color: var(--muted-foreground);
80
+ }
81
+ .pretui-contextring-ring {
82
+ --_contextring-ink: var(--muted-foreground);
83
+ width: 0.875rem;
84
+ height: 0.875rem;
85
+ border-radius: 50%;
86
+ background: conic-gradient(
87
+ var(--_contextring-ink) var(--pretui-ring-share),
88
+ var(--border-strong) 0
89
+ );
90
+ -webkit-mask: radial-gradient(
91
+ circle,
92
+ transparent 0.25rem,
93
+ black 0.28125rem
94
+ );
95
+ mask: radial-gradient(circle, transparent 0.25rem, black 0.28125rem);
96
+ }
97
+ /* past the warning share the ring (the mark) takes the attention
98
+ ink, and the digits step up to the full foreground */
99
+ .pretui-contextring[data-warn] {
100
+ color: var(--foreground);
101
+ }
102
+ .pretui-contextring[data-warn] .pretui-contextring-ring {
103
+ --_contextring-ink: var(--attention-ink);
104
+ }
105
+ }
106
+ </style>
107
+ </template>
108
+ }