@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,575 @@
1
+ // Pretui — UserBubble: the reader's message, as a white bubble that knows its width, length and delivery.
2
+ import Component from '@glimmer/component';
3
+ import { tracked } from '@glimmer/tracking';
4
+ import { on } from '@ember/modifier';
5
+ import { guidFor } from '@ember/object/internals';
6
+ import { modifier } from 'ember-modifier';
7
+ import { SpeakerLabel } from './speaker-label';
8
+ import ChevronDownIcon from '@cardstack/boxel-icons/chevron-down';
9
+ import { cssNumber, cssStyleFrom } from '../pretui-css';
10
+
11
+ export type UserBubbleDelivery =
12
+ | 'sent'
13
+ | 'pending'
14
+ | 'steer'
15
+ | 'queued'
16
+ | 'rejected';
17
+
18
+ export interface UserBubbleSignature {
19
+ Args: {
20
+ /** the message, as text. Or pass the message as the default block
21
+ * (rendered HTML, attachments), as the host does; with a block, still
22
+ * pass `@text` (the message's source) for Edit to start from */
23
+ text?: string;
24
+ /** lines shown before "Show more". Default: all */
25
+ maxLines?: number;
26
+ /** where the message is: 'sent' (default, says nothing), 'pending'
27
+ * (sending), 'steer' (read at the run's next step), 'queued' (sends
28
+ * when the run ends), 'rejected' (not sent) */
29
+ delivery?: UserBubbleDelivery;
30
+ /** why a rejected message was not sent */
31
+ reason?: string;
32
+ /** send a rejected message again */
33
+ onRetry?: () => void;
34
+ /** edit the message, which rewinds the thread to it: the bubble turns
35
+ * into an editor and hands the new text back. Edit shows only with
36
+ * `@text`, the source it edits; an unchanged draft saves nothing */
37
+ onEdit?: (text: string) => void;
38
+ /** when, as the caller formats it; on hover */
39
+ time?: string;
40
+ /** who posted it. Shown above the bubble, so a room where several
41
+ * people talk to the same assistant reads as one conversation with
42
+ * names, not as a special case */
43
+ author?: string;
44
+ /** the reader's own message: the label says "You" */
45
+ self?: boolean;
46
+ /** the same person as the message above: the name is not repeated */
47
+ continued?: boolean;
48
+ };
49
+ Blocks: {
50
+ /** the message itself, when it is more than text */
51
+ default: [];
52
+ /** the author's avatar over the bubble (the host's profile mark), in
53
+ * place of their initial */
54
+ avatar: [];
55
+ /** the context sent with it (CardPills), only when it changed from the
56
+ * turn before */
57
+ context: [];
58
+ /** what the reader attached to this message (CardPills, files),
59
+ * inside the bubble above the text, as the composer showed them */
60
+ attachments: [];
61
+ };
62
+ Element: HTMLDivElement;
63
+ }
64
+
65
+ const DELIVERY: Record<UserBubbleDelivery, string> = {
66
+ sent: '',
67
+ pending: 'Sending…',
68
+ steer: 'Steers the run at its next step',
69
+ queued: 'Queued · sends when the run ends',
70
+ rejected: 'Not sent',
71
+ };
72
+
73
+ /**
74
+ * The reader's message: main's white bubble in every theme, a share of the
75
+ * panel's width that grows on a narrow panel (92% at 320, 80%, 70% from
76
+ * 600), clamped to `@maxLines` with "Show more", and a quiet line under it
77
+ * while it is not yet simply sent.
78
+ */
79
+ export class UserBubble extends Component<UserBubbleSignature> {
80
+ @tracked expanded = false;
81
+ @tracked overflows = false;
82
+ @tracked editing = false;
83
+ @tracked draft = '';
84
+ readonly textId = guidFor(this) + '-text';
85
+
86
+ get delivery(): UserBubbleDelivery {
87
+ return this.args.delivery ?? 'sent';
88
+ }
89
+ get deliveryText(): string {
90
+ let text = DELIVERY[this.delivery];
91
+ return this.delivery === 'rejected' && this.args.reason
92
+ ? `${text} · ${this.args.reason}`
93
+ : text;
94
+ }
95
+ get clampStyle() {
96
+ let n = cssNumber(this.args.maxLines, 1, 1000);
97
+ return n && !this.expanded
98
+ ? cssStyleFrom([`--pretui-bubble-lines: ${Math.round(n)}`])
99
+ : undefined;
100
+ }
101
+ /** Edit needs the message's source to start from */
102
+ get canEdit(): boolean {
103
+ return Boolean(this.args.onEdit) && this.args.text !== undefined;
104
+ }
105
+ /** nothing to save: an empty draft */
106
+ get draftEmpty(): boolean {
107
+ return this.draft.trim() === '';
108
+ }
109
+ /** the name over the bubble: the author, "You" for the reader's own,
110
+ * none for a continued run */
111
+ get authorName(): string | undefined {
112
+ if (this.args.continued) {
113
+ return undefined;
114
+ }
115
+ return this.args.self ? 'You' : this.args.author;
116
+ }
117
+ get canRetry(): boolean {
118
+ return (
119
+ this.delivery === 'rejected' &&
120
+ Boolean(this.args.onRetry) &&
121
+ !this.editing
122
+ );
123
+ }
124
+ get clamped(): boolean {
125
+ return Boolean(this.args.maxLines) && !this.expanded;
126
+ }
127
+
128
+ // a clamped message shows "Show more" only when it really is cut
129
+ measure = modifier(
130
+ (
131
+ element: HTMLElement,
132
+ [_text, _clamped]: [string | undefined, boolean],
133
+ ) => {
134
+ if (typeof ResizeObserver !== 'function') {
135
+ return;
136
+ }
137
+ let check = () => {
138
+ this.overflows = element.scrollHeight > element.clientHeight + 1;
139
+ };
140
+ let observer = new ResizeObserver(check);
141
+ observer.observe(element);
142
+ return () => observer.disconnect();
143
+ },
144
+ );
145
+
146
+ toggle = () => (this.expanded = !this.expanded);
147
+ retry = () => this.args.onRetry?.();
148
+ /** set when the editor closes, so the Edit button that comes back takes
149
+ * focus; plain, not tracked, because the modifier reads it once */
150
+ private returnFocusToEdit = false;
151
+
152
+ startEdit = () => {
153
+ this.draft = this.args.text ?? '';
154
+ this.editing = true;
155
+ };
156
+ private closeEditor() {
157
+ this.returnFocusToEdit = true;
158
+ this.editing = false;
159
+ }
160
+ cancelEdit = () => this.closeEditor();
161
+ typeDraft = (event: Event) =>
162
+ (this.draft = (event.target as HTMLTextAreaElement).value);
163
+ /** an empty draft keeps the editor open; an unchanged one is a Cancel,
164
+ * since saving rewinds the thread */
165
+ saveEdit = () => {
166
+ let text = this.draft.trim();
167
+ if (!text) {
168
+ return;
169
+ }
170
+ if (text !== (this.args.text ?? '').trim()) {
171
+ this.args.onEdit?.(text);
172
+ }
173
+ this.closeEditor();
174
+ };
175
+ /** the editor's own keys stop here, so a host's document-level Escape
176
+ * does not also run */
177
+ editKey = (e: Event) => {
178
+ let event = e as KeyboardEvent;
179
+ if (event.key === 'Escape') {
180
+ event.preventDefault();
181
+ event.stopPropagation();
182
+ this.cancelEdit();
183
+ } else if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
184
+ event.preventDefault();
185
+ this.saveEdit();
186
+ }
187
+ };
188
+ focusField = modifier((element: HTMLTextAreaElement) => {
189
+ element.focus();
190
+ element.setSelectionRange(element.value.length, element.value.length);
191
+ });
192
+ focusEditOnReturn = modifier((element: HTMLButtonElement) => {
193
+ if (this.returnFocusToEdit) {
194
+ this.returnFocusToEdit = false;
195
+ element.focus();
196
+ }
197
+ });
198
+
199
+ <template>
200
+ <div
201
+ class='pretui-bubble-turn'
202
+ data-continued={{if @continued 'true'}}
203
+ data-delivery={{this.delivery}}
204
+ data-test-pretui-user-bubble={{this.delivery}}
205
+ ...attributes
206
+ >
207
+ {{#if this.authorName}}
208
+ {{#if (has-block 'avatar')}}
209
+ <SpeakerLabel
210
+ class='pretui-bubble-author'
211
+ @name={{this.authorName}}
212
+ @time={{@time}}
213
+ data-test-pretui-user-bubble-author={{this.authorName}}
214
+ >
215
+ <:mark>{{yield to='avatar'}}</:mark>
216
+ </SpeakerLabel>
217
+ {{else}}
218
+ <SpeakerLabel
219
+ class='pretui-bubble-author'
220
+ @name={{this.authorName}}
221
+ @time={{@time}}
222
+ data-test-pretui-user-bubble-author={{this.authorName}}
223
+ />
224
+ {{/if}}
225
+ {{/if}}
226
+ {{#if this.editing}}
227
+ <div
228
+ class='pretui-bubble pretui-bubble-editor'
229
+ data-test-pretui-user-bubble-editor
230
+ >
231
+ <textarea
232
+ class='pretui-bubble-field'
233
+ aria-label='Edit your message'
234
+ rows='3'
235
+ value={{this.draft}}
236
+ {{on 'input' this.typeDraft}}
237
+ {{on 'keydown' this.editKey}}
238
+ {{this.focusField}}
239
+ ></textarea>
240
+ <p class='pretui-bubble-editnote'>Saving rewinds the thread to this
241
+ message.</p>
242
+ <div class='pretui-bubble-editactions'>
243
+ <button
244
+ type='button'
245
+ class='pretui-bubble-act'
246
+ {{on 'click' this.cancelEdit}}
247
+ data-test-pretui-user-bubble-cancel
248
+ >Cancel</button>
249
+ <button
250
+ type='button'
251
+ class='pretui-bubble-act'
252
+ data-kind='primary'
253
+ disabled={{this.draftEmpty}}
254
+ {{on 'click' this.saveEdit}}
255
+ data-test-pretui-user-bubble-save
256
+ >Save and resend</button>
257
+ </div>
258
+ </div>
259
+ {{else}}
260
+ <div class='pretui-bubble'>
261
+ {{#if (has-block 'attachments')}}
262
+ <div
263
+ class='pretui-bubble-attachments'
264
+ data-test-pretui-user-bubble-attachments
265
+ >{{yield to='attachments'}}</div>
266
+ {{/if}}
267
+ {{#if (has-block)}}
268
+ <div
269
+ class='pretui-bubble-text'
270
+ data-rich='true'
271
+ id={{this.textId}}
272
+ data-clamped={{if this.clamped 'true'}}
273
+ style={{this.clampStyle}}
274
+ {{this.measure @text this.clamped}}
275
+ data-test-pretui-user-bubble-text
276
+ >{{yield}}</div>
277
+ {{else}}
278
+ <p
279
+ class='pretui-bubble-text'
280
+ id={{this.textId}}
281
+ data-clamped={{if this.clamped 'true'}}
282
+ style={{this.clampStyle}}
283
+ {{this.measure @text this.clamped}}
284
+ data-test-pretui-user-bubble-text
285
+ >{{@text}}</p>
286
+ {{/if}}
287
+ {{#if (if @maxLines (if this.expanded true this.overflows))}}
288
+ <button
289
+ type='button'
290
+ class='pretui-bubble-more'
291
+ aria-expanded={{if this.expanded 'true' 'false'}}
292
+ aria-controls={{this.textId}}
293
+ {{on 'click' this.toggle}}
294
+ data-test-pretui-user-bubble-more
295
+ ><span>{{if
296
+ this.expanded
297
+ 'Show less'
298
+ 'Show more'
299
+ }}</span><ChevronDownIcon
300
+ class='pretui-bubble-more-icon'
301
+ width='12'
302
+ height='12'
303
+ aria-hidden='true'
304
+ /></button>
305
+ {{/if}}
306
+ </div>
307
+ {{/if}}
308
+ {{#if (has-block 'context')}}
309
+ <div class='pretui-bubble-context'>{{yield to='context'}}</div>
310
+ {{/if}}
311
+ <div class='pretui-bubble-foot'>
312
+ {{#if this.deliveryText}}
313
+ <span
314
+ class='pretui-bubble-delivery'
315
+ role='status'
316
+ data-test-pretui-user-bubble-delivery
317
+ >{{this.deliveryText}}</span>
318
+ {{/if}}
319
+ {{#if this.canRetry}}
320
+ <button
321
+ type='button'
322
+ class='pretui-bubble-link'
323
+ {{on 'click' this.retry}}
324
+ data-test-pretui-user-bubble-retry
325
+ >Retry</button>
326
+ {{/if}}
327
+ {{#if this.canEdit}}{{#unless this.editing}}
328
+ <button
329
+ type='button'
330
+ class='pretui-bubble-link pretui-bubble-hover'
331
+ {{on 'click' this.startEdit}}
332
+ {{this.focusEditOnReturn}}
333
+ data-test-pretui-user-bubble-edit
334
+ >Edit</button>
335
+ {{/unless}}{{/if}}
336
+ {{#if (if @time (if this.authorName false true))}}<span
337
+ class='pretui-bubble-time pretui-bubble-hover'
338
+ >{{@time}}</span>{{/if}}
339
+ </div>
340
+ </div>
341
+ <style scoped>
342
+ @layer PretComponent {
343
+ /* the turn row is full width and a size container, so the bubble's
344
+ share follows the panel, not the window */
345
+ .pretui-bubble-turn {
346
+ container-type: inline-size;
347
+ display: grid;
348
+ /* left-justified, as main draws it: its one small corner, top
349
+ left, points at the edge it hangs from */
350
+ justify-items: start;
351
+ gap: var(--boxel-sp-4xs);
352
+ }
353
+ /* a continued run sits closer to the message above it */
354
+ .pretui-bubble-turn[data-continued] {
355
+ margin-block-start: calc(
356
+ -1 * var(--pretui-block-gap, var(--boxel-sp-sm)) +
357
+ var(--boxel-sp-4xs)
358
+ );
359
+ }
360
+ /* the reader's bubble steps back: the theme's inset with ink-1
361
+ text at reading size, holding at 70% of the panel */
362
+ .pretui-bubble {
363
+ box-sizing: border-box;
364
+ max-width: 70%;
365
+ padding: var(--boxel-sp-xs) var(--boxel-sp-sm);
366
+ border-radius: var(--boxel-border-radius-lg);
367
+ border-top-left-radius: var(--boxel-border-radius-xs);
368
+ background-color: var(--pretui-bubble-bg, var(--inset));
369
+ color: var(--pretui-bubble-ink, var(--foreground));
370
+ font-size: var(--boxel-body-font-size);
371
+ line-height: var(--boxel-body-line-height);
372
+ font-weight: 400;
373
+ }
374
+ .pretui-bubble-text {
375
+ margin: 0;
376
+ white-space: pre-wrap;
377
+ overflow-wrap: anywhere;
378
+ }
379
+ /* rich content keeps its own blocks; only text pre-wraps */
380
+ .pretui-bubble-text[data-rich] {
381
+ white-space: normal;
382
+ }
383
+ .pretui-bubble-text[data-rich] > :deep(:first-child) {
384
+ margin-top: 0;
385
+ }
386
+ .pretui-bubble-text[data-rich] > :deep(:last-child) {
387
+ margin-bottom: 0;
388
+ }
389
+ .pretui-bubble-text[data-clamped] {
390
+ display: -webkit-box;
391
+ -webkit-box-orient: vertical;
392
+ -webkit-line-clamp: var(--pretui-bubble-lines);
393
+ overflow: hidden;
394
+ }
395
+ .pretui-bubble-attachments {
396
+ display: flex;
397
+ flex-wrap: wrap;
398
+ gap: var(--boxel-sp-4xs);
399
+ margin-bottom: var(--boxel-sp-xs);
400
+ }
401
+ /* a host that always passes the block, with nothing to attach this
402
+ time, gets no empty line */
403
+ .pretui-bubble-attachments:empty {
404
+ display: none;
405
+ }
406
+ /* a clamped message fades into its Show more, so the cut reads as a
407
+ cut */
408
+ .pretui-bubble-text[data-clamped] {
409
+ mask-image: linear-gradient(
410
+ to bottom,
411
+ black calc(100% - var(--boxel-body-line-height)),
412
+ color-mix(in srgb, black 35%, transparent)
413
+ );
414
+ }
415
+ /* Show more: a quiet control in ink-2 with a chevron, so it never
416
+ reads as the message's last word */
417
+ .pretui-bubble-more {
418
+ display: inline-flex;
419
+ align-items: center;
420
+ gap: var(--boxel-sp-6xs);
421
+ margin-top: var(--boxel-sp-4xs);
422
+ margin-inline-start: calc(-1 * var(--boxel-sp-xs));
423
+ height: 1.5rem;
424
+ padding: 0 var(--boxel-sp-xs);
425
+ border: 0;
426
+ border-radius: var(--boxel-border-radius-sm);
427
+ background-color: transparent;
428
+ font-family: inherit;
429
+ font-size: var(--boxel-ui-label-font-size);
430
+ line-height: var(--boxel-ui-label-line-height);
431
+ font-weight: 400;
432
+ color: var(--muted-foreground);
433
+ cursor: pointer;
434
+ }
435
+ .pretui-bubble-more:hover {
436
+ background-color: var(--hover);
437
+ color: var(--foreground);
438
+ }
439
+ .pretui-bubble-more[aria-expanded='true'] .pretui-bubble-more-icon {
440
+ transform: rotate(180deg);
441
+ }
442
+ /* not yet delivered: the text steps down to ink-2; the line under
443
+ the bubble says why */
444
+ .pretui-bubble-turn[data-delivery='pending'] .pretui-bubble,
445
+ .pretui-bubble-turn[data-delivery='queued'] .pretui-bubble {
446
+ color: var(--muted-foreground);
447
+ }
448
+ .pretui-bubble-context {
449
+ display: flex;
450
+ justify-content: flex-start;
451
+ max-width: 92%;
452
+ }
453
+ .pretui-bubble-foot {
454
+ display: flex;
455
+ align-items: baseline;
456
+ gap: var(--boxel-sp-xs);
457
+ font-size: var(--boxel-caption-font-size);
458
+ line-height: var(--boxel-caption-line-height);
459
+ font-variant-numeric: tabular-nums;
460
+ color: var(--subtle-foreground);
461
+ }
462
+ /* a state colours the word, never the bubble */
463
+ .pretui-bubble-turn[data-delivery='steer'] .pretui-bubble-delivery {
464
+ color: var(--attention-ink);
465
+ }
466
+ .pretui-bubble-turn[data-delivery='rejected'] .pretui-bubble-delivery {
467
+ color: var(--destructive-ink);
468
+ }
469
+ /* a text action, 1.5rem tall so it is a whole pointer target */
470
+ .pretui-bubble-link {
471
+ display: inline-flex;
472
+ align-items: center;
473
+ min-height: 1.5rem;
474
+ padding: 0;
475
+ border: 0;
476
+ background-color: transparent;
477
+ font-family: inherit;
478
+ font-size: inherit;
479
+ line-height: inherit;
480
+ font-weight: 600;
481
+ color: var(--muted-foreground);
482
+ cursor: pointer;
483
+ }
484
+ .pretui-bubble-link:hover {
485
+ color: var(--foreground);
486
+ }
487
+ .pretui-bubble-link:focus-visible,
488
+ .pretui-bubble-more:focus-visible,
489
+ .pretui-bubble-act:focus-visible {
490
+ outline: 2px solid var(--ring);
491
+ outline-offset: 1px;
492
+ }
493
+ /* a sent message's foot holds only hover items: it takes no line,
494
+ and they hang in the gap below the bubble */
495
+ .pretui-bubble-turn[data-delivery='sent'] .pretui-bubble-foot {
496
+ height: 0;
497
+ margin-top: calc(-1 * var(--boxel-sp-4xs));
498
+ overflow: visible;
499
+ }
500
+ /* edit and the time are for the reader who looks for them */
501
+ .pretui-bubble-hover {
502
+ opacity: 0;
503
+ }
504
+ .pretui-bubble-turn:hover .pretui-bubble-hover,
505
+ .pretui-bubble-turn:focus-within .pretui-bubble-hover {
506
+ opacity: 1;
507
+ }
508
+ @media (hover: none) {
509
+ .pretui-bubble-hover {
510
+ opacity: 1;
511
+ }
512
+ }
513
+ .pretui-bubble-editor {
514
+ display: grid;
515
+ gap: var(--boxel-sp-xs);
516
+ width: 100%;
517
+ max-width: 100%;
518
+ }
519
+ .pretui-bubble-field {
520
+ box-sizing: border-box;
521
+ width: 100%;
522
+ padding: var(--boxel-sp-4xs) var(--boxel-sp-xs);
523
+ border: 0;
524
+ border-radius: var(--boxel-border-radius-sm);
525
+ background-color: var(--field);
526
+ box-shadow: inset 0 0 0 1px var(--border);
527
+ font-family: inherit;
528
+ font-size: inherit;
529
+ line-height: inherit;
530
+ color: inherit;
531
+ resize: vertical;
532
+ }
533
+ .pretui-bubble-field:focus {
534
+ outline: 2px solid var(--ring);
535
+ outline-offset: 1px;
536
+ }
537
+ .pretui-bubble-editnote {
538
+ margin: 0;
539
+ font-size: var(--boxel-caption-font-size);
540
+ line-height: var(--boxel-caption-line-height);
541
+ color: var(--muted-foreground);
542
+ }
543
+ .pretui-bubble-editactions {
544
+ display: flex;
545
+ justify-content: flex-end;
546
+ gap: var(--boxel-sp-xs);
547
+ }
548
+ .pretui-bubble-act {
549
+ height: 1.75rem;
550
+ padding: 0 var(--boxel-sp-sm);
551
+ border: 0;
552
+ border-radius: var(--boxel-border-radius-sm);
553
+ background-color: transparent;
554
+ box-shadow: inset 0 0 0 1px var(--border);
555
+ font-family: inherit;
556
+ font-size: var(--boxel-ui-label-font-size);
557
+ line-height: var(--boxel-ui-label-line-height);
558
+ font-weight: 600;
559
+ color: inherit;
560
+ cursor: pointer;
561
+ }
562
+ /* Save and resend is the moment's one action */
563
+ .pretui-bubble-act[data-kind='primary'] {
564
+ box-shadow: none;
565
+ background-color: var(--primary);
566
+ color: var(--primary-foreground);
567
+ }
568
+ .pretui-bubble-act:disabled {
569
+ opacity: 0.5;
570
+ cursor: default;
571
+ }
572
+ }
573
+ </style>
574
+ </template>
575
+ }