@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,2087 @@
1
+ // Pretui — ToolRow: one tool call in an agent transcript. WorkItem's state vocabulary, plus a subtitle naming what the tool acted on, a disclosure the reader owns once they touch it, a lazily mounted body, and the skill that granted the tool.
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 { htmlSafe } from '@ember/template';
7
+ import type { SafeString } from '@ember/template';
8
+ import type { ComponentLike } from '@glint/template';
9
+ import { modifier } from 'ember-modifier';
10
+ import { Collapse } from '../internal/agentic-chat';
11
+ import { scheduleMount } from '../internal/mount-queue';
12
+ import type { ToolKind, ToolPhrase } from '../internal/tool-phrases';
13
+ export type { ToolKind, ToolPhrase } from '../internal/tool-phrases';
14
+ import ArrowRight from '@cardstack/boxel-icons/arrow-right';
15
+
16
+ import { Chip } from './chip';
17
+ import { CodeBlock } from './code-block';
18
+ import { DiffBlock, lineDiff } from './diff-block';
19
+ import { StateGlyph } from './state-glyph';
20
+ import { VisuallyHidden } from './visually-hidden';
21
+ import type { DiffBlockSignature, DiffLine } from './diff-block';
22
+ import { PlanMark } from './plan-mark';
23
+ import { toolIcon } from './tool-icon';
24
+ import type { ToolIcon } from './tool-icon';
25
+ import { workStatus, workTone } from './work-item';
26
+ import type { WorkTone } from './work-item';
27
+
28
+ // ── Renderer registry ────────────────────────────────────────────────────
29
+ // An argument, never a global: a realm can host several apps, each with its
30
+ // own tools. ToolGroup passes one registry down to every row; a ToolRow used
31
+ // alone can take @title, @subtitle and <:body> directly instead.
32
+
33
+ export interface ToolBodySignature {
34
+ Args: {
35
+ /** the call's arguments: parsed, or the raw text while still streaming */
36
+ params?: unknown;
37
+ /** the call's result, once there is one */
38
+ result?: unknown;
39
+ state: string;
40
+ };
41
+ }
42
+
43
+ export interface ToolResultSignature {
44
+ Args: {
45
+ /** the call's arguments, partial while they stream */
46
+ params?: unknown;
47
+ result?: unknown;
48
+ /** 'streaming' while the arguments arrive */
49
+ state: string;
50
+ /** answer the call from inside its card: present only while the call
51
+ * waits on the reader, so a settled card cannot answer twice */
52
+ resolve?: (value: unknown) => unknown;
53
+ };
54
+ }
55
+
56
+ export interface ToolRenderer {
57
+ /** the tool's display name: 'Read', 'Patch fields' */
58
+ title?: string;
59
+ /** derive the subtitle (the thing the tool acted on) from the arguments */
60
+ subtitle?: (params: unknown) => string | undefined;
61
+ /** the body that renders arguments and result */
62
+ body?: ComponentLike<ToolBodySignature>;
63
+ /** what the tool does, which ToolGroup uses to phrase a run ("Read 3
64
+ * files, ran 2 commands") and to merge runs of reads and of writes.
65
+ * Default 'other' */
66
+ kind?: ToolKind;
67
+ /** the tool's own phrase, when its kind's is not right: { active:
68
+ * 'Cupping', done: 'Cupped', one: 'lot', many: 'lots' } */
69
+ phrase?: ToolPhrase;
70
+ /** the tool's UI: rendered into `<:result>` under the row's quiet line,
71
+ * usually a CardEmbed or LiveCard around the card the call made. A
72
+ * host's own `<:result>` block wins */
73
+ result?: ComponentLike<ToolResultSignature>;
74
+ /** 'standalone' (default when `result` is set): the call's card is
75
+ * surfaced in the turn, so a grouping helper never folds the call into
76
+ * a ToolGroup; 'inline' lets it fold */
77
+ display?: 'inline' | 'standalone';
78
+ }
79
+
80
+ export type ToolRenderers = Record<string, ToolRenderer>;
81
+
82
+ export interface ToolSource {
83
+ kind: 'skill' | 'builtin';
84
+ name: string;
85
+ }
86
+
87
+ /** How much of the transcript a call claims, set by the tool, never by the
88
+ * message: a `step` reads or moves the UI, an `action` writes, a `run` works
89
+ * in the background and outlives the page, a `script` is a program that can
90
+ * do all three. */
91
+ export type ToolDensity = 'step' | 'action' | 'run' | 'script';
92
+
93
+ /** A multi-step call reporting where it is: step `step` of `of`, and how far
94
+ * through the current step (0–1, default 0.5). The bar fills only as steps
95
+ * finish, so it is never full while work remains. */
96
+ export interface ToolProgress {
97
+ step: number;
98
+ of: number;
99
+ label?: string;
100
+ fraction?: number;
101
+ }
102
+
103
+ /** "Step 2 of 4 · Reading menus"; the label is optional. */
104
+ export function progressText(progress: ToolProgress): string {
105
+ let head = `Step ${progress.step} of ${progress.of}`;
106
+ return progress.label ? `${head} · ${progress.label}` : head;
107
+ }
108
+
109
+ /** The bar's fill, 0–100: finished steps plus the current one's fraction,
110
+ * capped below 100 until the last step is done. */
111
+ export function progressPercent(progress: ToolProgress): number {
112
+ let of = Math.max(1, progress.of);
113
+ let step = Math.min(Math.max(1, progress.step), of);
114
+ let fraction = Math.min(Math.max(progress.fraction ?? 0.5, 0), 1);
115
+ return Math.round(((step - 1 + fraction) / of) * 100);
116
+ }
117
+
118
+ /** 'read_realm_file' → 'Read realm file'; 'patchFields' → 'Patch fields'. */
119
+ export function humanizeToolName(name: string): string {
120
+ let words = name
121
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
122
+ .replace(/[_\-.:/]+/g, ' ')
123
+ .trim()
124
+ .toLowerCase();
125
+ return words ? words[0]!.toUpperCase() + words.slice(1) : name;
126
+ }
127
+
128
+ /** Seconds as a short duration: 0.4s, 12s, 2m 05s. */
129
+ export function formatToolDuration(seconds: number): string {
130
+ if (!Number.isFinite(seconds) || seconds < 0) {
131
+ return '';
132
+ }
133
+ if (seconds < 10) {
134
+ return `${Math.round(seconds * 10) / 10}s`;
135
+ }
136
+ if (seconds < 60) {
137
+ return `${Math.round(seconds)}s`;
138
+ }
139
+ let minutes = Math.floor(seconds / 60);
140
+ let rest = Math.round(seconds % 60);
141
+ return `${minutes}m ${String(rest).padStart(2, '0')}s`;
142
+ }
143
+
144
+ /** True when any value is truthy: a template helper, since Glimmer has no `or`. */
145
+ const either = (...values: unknown[]): boolean => values.some(Boolean);
146
+
147
+ function asText(value: unknown): string {
148
+ if (typeof value === 'string') {
149
+ return value;
150
+ }
151
+ try {
152
+ return JSON.stringify(value, null, 2) ?? String(value);
153
+ } catch {
154
+ return String(value);
155
+ }
156
+ }
157
+
158
+ // ── GenericToolBody ──────────────────────────────────────────────────────
159
+ // What a tool with no renderer shows: its arguments and its result as code.
160
+ // Arguments that are still a string are mid-stream, so they render as the
161
+ // plain text they are rather than as a JSON parse that would fail.
162
+
163
+ class GenericToolBody extends Component<ToolBodySignature> {
164
+ get hasArgs() {
165
+ return this.args.params !== undefined && this.args.params !== '';
166
+ }
167
+ get hasResult() {
168
+ return this.args.result !== undefined;
169
+ }
170
+ get argsText() {
171
+ return asText(this.args.params);
172
+ }
173
+ get argsLanguage() {
174
+ return typeof this.args.params === 'string' ? undefined : 'json';
175
+ }
176
+ get resultText() {
177
+ return asText(this.args.result);
178
+ }
179
+ get resultLanguage() {
180
+ return typeof this.args.result === 'string' ? undefined : 'json';
181
+ }
182
+ <template>
183
+ <div class='pretui-toolrow-generic' data-test-pretui-tool-row-generic>
184
+ {{#if this.hasArgs}}
185
+ <CodeBlock
186
+ @code={{this.argsText}}
187
+ @language={{this.argsLanguage}}
188
+ @label='Arguments'
189
+ @maxLines={{12}}
190
+ @wrap={{true}}
191
+ data-scroll-region
192
+ />
193
+ {{/if}}
194
+ {{#if this.hasResult}}
195
+ <CodeBlock
196
+ @code={{this.resultText}}
197
+ @language={{this.resultLanguage}}
198
+ @label='Result'
199
+ @maxLines={{12}}
200
+ @wrap={{true}}
201
+ data-scroll-region
202
+ />
203
+ {{/if}}
204
+ </div>
205
+ <style scoped>
206
+ @layer PretComponent {
207
+ .pretui-toolrow-generic {
208
+ display: flex;
209
+ flex-direction: column;
210
+ gap: var(--boxel-sp-xs);
211
+ }
212
+ }
213
+ </style>
214
+ </template>
215
+ }
216
+
217
+ // ── ToolLine ─────────────────────────────────────────────────────────────
218
+ // One thing that happened inside a call: a page read, a failure with its
219
+ // reason, an auto-fix, a repair. It sits on the row's grid: a glyph column
220
+ // as wide as the row's disc, the same gap, then the text. Nesting is a
221
+ // hairline rail under the parent's glyph, and the child's glyph lands under
222
+ // the parent's text, so any depth lines up with the rows around it.
223
+
224
+ export type ToolLineTone = 'done' | 'live' | 'failed' | 'attention' | 'muted';
225
+
226
+ export interface ToolLineSignature {
227
+ Args: {
228
+ /** colours the glyph, and for failed, attention and muted the text */
229
+ tone?: ToolLineTone;
230
+ /** the line's mark: ✓ ✕ ↻ ! ? for outcomes; ◐ (or 'current') for a
231
+ * line under way, drawn as the thin turning ring; · ○ or none for a note,
232
+ * which keeps the column empty */
233
+ glyph?: string;
234
+ /** 0 (default) on the row's text edge; 1–3 nests under the line above */
235
+ depth?: number;
236
+ /** a second, quieter line: the model used, a size, a count */
237
+ sub?: string;
238
+ /** set the text in the mono voice, for paths and calls */
239
+ mono?: boolean;
240
+ /** an image the line made (a written PNG, a generated cover), shown
241
+ * small under its text */
242
+ thumb?: string;
243
+ /** the thumbnail's alternative text. Default: none (decorative, since
244
+ * the line's text already names the file) */
245
+ thumbAlt?: string;
246
+ };
247
+ Blocks: { default: [] };
248
+ Element: HTMLDivElement;
249
+ }
250
+
251
+ // the glyphs that mark a note, not an outcome: they draw nothing
252
+ const NOTE_GLYPHS = new Set([
253
+ '',
254
+ '·',
255
+ '○',
256
+ 'next',
257
+ 'note',
258
+ 'skipped',
259
+ '–',
260
+ '-',
261
+ ]);
262
+ const UNDER_WAY_GLYPHS = new Set(['◐', 'current']);
263
+
264
+ // the glyph and its colour are decorative, so a tone a reader must not miss
265
+ // is also said in words, before the line's text
266
+ const SPOKEN_TONES: Partial<Record<ToolLineTone, string>> = {
267
+ failed: 'Failed:',
268
+ attention: 'Needs attention:',
269
+ };
270
+
271
+ export class ToolLine extends Component<ToolLineSignature> {
272
+ get underWay(): boolean {
273
+ return UNDER_WAY_GLYPHS.has(this.args.glyph ?? '');
274
+ }
275
+ get marked(): boolean {
276
+ return !NOTE_GLYPHS.has(this.args.glyph ?? '');
277
+ }
278
+ get depth(): number {
279
+ return Math.min(Math.max(Math.round(this.args.depth ?? 0), 0), 3);
280
+ }
281
+ get spokenTone(): string | undefined {
282
+ return this.args.tone ? SPOKEN_TONES[this.args.tone] : undefined;
283
+ }
284
+ <template>
285
+ <div
286
+ class='pretui-toolline'
287
+ data-tone={{@tone}}
288
+ data-depth={{if this.depth this.depth}}
289
+ data-mono={{if @mono 'true'}}
290
+ data-test-pretui-tool-line
291
+ ...attributes
292
+ >
293
+ {{! outcomes keep their mark; a line under way turns the thin ring;
294
+ a note (· ○ or none) has no mark, only the text edge }}
295
+ {{#if this.underWay}}
296
+ <PlanMark class='pretui-toolline-glyph' @state='current' @size={{12}} />
297
+ {{else if this.marked}}
298
+ <StateGlyph class='pretui-toolline-glyph' @glyph={{@glyph}} />
299
+ {{else}}
300
+ <span class='pretui-toolline-glyph' aria-hidden='true'></span>
301
+ {{/if}}
302
+ <span class='pretui-toolline-text'>
303
+ {{#if this.spokenTone}}<VisuallyHidden
304
+ data-test-pretui-tool-line-tone
305
+ >{{this.spokenTone}} </VisuallyHidden>{{/if}}
306
+ {{yield}}
307
+ {{#if @sub}}<span
308
+ class='pretui-toolline-sub'
309
+ data-test-pretui-tool-line-sub
310
+ >{{@sub}}</span>{{/if}}
311
+ {{#if @thumb}}<img
312
+ class='pretui-toolline-thumb'
313
+ src={{@thumb}}
314
+ alt={{if @thumbAlt @thumbAlt ''}}
315
+ loading='lazy'
316
+ data-test-pretui-tool-line-thumb
317
+ />{{/if}}
318
+ </span>
319
+ </div>
320
+ <style scoped>
321
+ @layer PretComponent {
322
+ .pretui-toolline {
323
+ /* a ToolLine reads the row's grid; alone it falls back to it */
324
+ --pretui-toolline-disc: var(--pretui-toolrow-disc, var(--boxel-sp));
325
+ --pretui-toolline-gap: var(--pretui-toolrow-gap, var(--boxel-sp-xs));
326
+ --pretui-toolline-step: calc(
327
+ var(--pretui-toolline-disc) + var(--pretui-toolline-gap)
328
+ );
329
+ display: grid;
330
+ grid-template-columns: var(--pretui-toolline-disc) minmax(0, 1fr);
331
+ column-gap: var(--pretui-toolline-gap);
332
+ align-items: baseline;
333
+ font-size: var(--boxel-ui-label-font-size);
334
+ line-height: var(--boxel-ui-label-line-height);
335
+ /* what happened reads in the settled ink; only a line still in
336
+ progress takes the bright foreground */
337
+ color: var(--pretui-toolrow-settled-ink, var(--foreground));
338
+ }
339
+ .pretui-toolline[data-tone='live'] {
340
+ color: var(--foreground);
341
+ }
342
+ /* the rail sits under the parent glyph's centre; the padding then
343
+ carries this glyph to the parent's text edge */
344
+ .pretui-toolline[data-depth] {
345
+ box-shadow: inset 1px 0 0 var(--border);
346
+ padding-inline-start: calc(
347
+ var(--pretui-toolline-step) - var(--pretui-toolline-disc) / 2
348
+ );
349
+ }
350
+ .pretui-toolline[data-depth='1'] {
351
+ margin-inline-start: calc(var(--pretui-toolline-disc) / 2);
352
+ }
353
+ .pretui-toolline[data-depth='2'] {
354
+ margin-inline-start: calc(
355
+ var(--pretui-toolline-step) + var(--pretui-toolline-disc) / 2
356
+ );
357
+ box-shadow: inset 1px 0 0 var(--border-strong);
358
+ }
359
+ .pretui-toolline[data-depth='3'] {
360
+ margin-inline-start: calc(
361
+ var(--pretui-toolline-step) * 2 + var(--pretui-toolline-disc) / 2
362
+ );
363
+ box-shadow: inset 1px 0 0 var(--border-strong);
364
+ }
365
+ .pretui-toolline-glyph {
366
+ text-align: center;
367
+ font-size: var(--boxel-caption-font-size);
368
+ color: var(--subtle-foreground);
369
+ }
370
+ /* done is ink with its check, not green; a state colours the mark
371
+ only, and the line's words stay in the row's ink */
372
+ .pretui-toolline[data-tone='done'] .pretui-toolline-glyph {
373
+ color: var(--foreground);
374
+ }
375
+ .pretui-toolline[data-tone='live'] .pretui-toolline-glyph {
376
+ color: var(--primary-ink);
377
+ }
378
+ .pretui-toolline[data-tone='failed'] .pretui-toolline-glyph {
379
+ color: var(--destructive-ink);
380
+ }
381
+ .pretui-toolline[data-tone='attention'] .pretui-toolline-glyph {
382
+ color: var(--attention-ink);
383
+ }
384
+ .pretui-toolline[data-tone='muted'] .pretui-toolline-text {
385
+ color: var(--muted-foreground);
386
+ }
387
+ .pretui-toolline[data-mono] .pretui-toolline-text {
388
+ font-family: var(--font-mono);
389
+ font-size: var(--boxel-font-size-xs);
390
+ line-height: var(--boxel-line-height-xs);
391
+ }
392
+ .pretui-toolline-text {
393
+ min-width: 0;
394
+ overflow-wrap: anywhere;
395
+ }
396
+ /* square, small, inside the text column: a look, not a preview */
397
+ .pretui-toolline-thumb {
398
+ display: block;
399
+ width: 4.5rem;
400
+ height: 4.5rem;
401
+ margin-block: var(--boxel-sp-4xs) var(--boxel-sp-6xs);
402
+ border-radius: var(--boxel-border-radius-xs);
403
+ object-fit: cover;
404
+ background-color: var(--inset);
405
+ box-shadow: 0 0 0 1px var(--border);
406
+ }
407
+ .pretui-toolline-sub {
408
+ display: block;
409
+ font-family: var(--font-sans);
410
+ font-size: var(--boxel-caption-font-size);
411
+ line-height: var(--boxel-caption-line-height);
412
+ font-variant-numeric: tabular-nums;
413
+ color: var(--subtle-foreground);
414
+ }
415
+ }
416
+ </style>
417
+ </template>
418
+ }
419
+
420
+ // ── ToolChange ───────────────────────────────────────────────────────────
421
+ // One value a call changed: the field's label, what it was, what it is now.
422
+ // A write says what changed, never only which paths it touched. It sits on
423
+ // the same grid as ToolLine, its label column in the glyph's place widened
424
+ // to a fixed measure so a stack of changes lines up.
425
+
426
+ export interface ToolChangeSignature {
427
+ Args: {
428
+ /** the field's label, as the card shows it: "Base width" */
429
+ label: string;
430
+ /** the value before; omit when it is not known */
431
+ from?: string;
432
+ /** the value after */
433
+ to?: string;
434
+ /** "×42": the change applies to that many cards */
435
+ count?: string;
436
+ /** how sure the assistant is of the new value: 'guessed' (inferred,
437
+ * not read) or 'unclear' (it found more than one answer). Omit when
438
+ * it read the value plainly */
439
+ confidence?: 'guessed' | 'unclear';
440
+ };
441
+ Element: HTMLDivElement;
442
+ }
443
+
444
+ export class ToolChange extends Component<ToolChangeSignature> {
445
+ <template>
446
+ <div class='pretui-toolchange' data-test-pretui-tool-change ...attributes>
447
+ <span
448
+ class='pretui-toolchange-label'
449
+ title={{@label}}
450
+ data-test-pretui-tool-change-label
451
+ >{{@label}}</span>
452
+ <span class='pretui-toolchange-values'>
453
+ {{#if @from}}
454
+ <del
455
+ class='pretui-toolchange-from'
456
+ data-test-pretui-tool-change-from
457
+ >{{@from}}</del>
458
+ <ArrowRight
459
+ class='pretui-toolchange-arrow'
460
+ width='12'
461
+ height='12'
462
+ role='img'
463
+ aria-label='changed to'
464
+ />
465
+ {{/if}}
466
+ {{#if @to}}<ins
467
+ class='pretui-toolchange-to'
468
+ data-test-pretui-tool-change-to
469
+ >{{@to}}</ins>{{/if}}
470
+ {{#if @count}}<span
471
+ class='pretui-toolchange-count'
472
+ >{{@count}}</span>{{/if}}
473
+ {{#if @confidence}}<span
474
+ class='pretui-toolchange-confidence'
475
+ data-confidence={{@confidence}}
476
+ data-test-pretui-tool-change-confidence={{@confidence}}
477
+ >{{@confidence}}</span>{{/if}}
478
+ </span>
479
+ </div>
480
+ <style scoped>
481
+ @layer PretComponent {
482
+ .pretui-toolchange {
483
+ display: grid;
484
+ grid-template-columns: minmax(0, 7.5em) minmax(0, 1fr);
485
+ column-gap: var(--boxel-sp-sm);
486
+ align-items: baseline;
487
+ font-size: var(--boxel-ui-label-font-size);
488
+ line-height: var(--boxel-ui-label-line-height);
489
+ }
490
+ .pretui-toolchange-label {
491
+ color: var(--muted-foreground);
492
+ white-space: nowrap;
493
+ overflow: hidden;
494
+ text-overflow: ellipsis;
495
+ }
496
+ .pretui-toolchange-values {
497
+ min-width: 0;
498
+ overflow-wrap: anywhere;
499
+ /* a long value wraps to the width, up to three lines */
500
+ display: -webkit-box;
501
+ -webkit-box-orient: vertical;
502
+ -webkit-line-clamp: 3;
503
+ overflow: hidden;
504
+ }
505
+ /* the values are field values, so they quote in the mono voice */
506
+ .pretui-toolchange-from,
507
+ .pretui-toolchange-to {
508
+ font-family: var(--font-mono);
509
+ font-size: var(--boxel-font-size-xs);
510
+ line-height: var(--boxel-line-height-xs);
511
+ }
512
+ .pretui-toolchange-from {
513
+ color: var(--muted-foreground);
514
+ text-decoration-color: var(--subtle-foreground);
515
+ }
516
+ .pretui-toolchange-arrow {
517
+ margin-inline: var(--boxel-sp-4xs);
518
+ vertical-align: -0.1em;
519
+ color: var(--subtle-foreground);
520
+ }
521
+ .pretui-toolchange-to {
522
+ text-decoration: none;
523
+ font-weight: 400;
524
+ color: var(--foreground);
525
+ }
526
+ /* a value the assistant did not read plainly says so, in the
527
+ attention ink: the reader checks it before keeping it */
528
+ .pretui-toolchange-confidence {
529
+ margin-inline-start: var(--boxel-sp-xs);
530
+ padding: 0 var(--boxel-sp-4xs);
531
+ border-radius: var(--boxel-border-radius-xs);
532
+ box-shadow: inset 0 0 0 1px var(--attention);
533
+ font-size: var(--boxel-caption-font-size);
534
+ line-height: var(--boxel-caption-line-height);
535
+ color: var(--attention-ink);
536
+ }
537
+ .pretui-toolchange-count {
538
+ margin-inline-start: var(--boxel-sp-xs);
539
+ font-size: var(--boxel-caption-font-size);
540
+ line-height: var(--boxel-caption-line-height);
541
+ font-variant-numeric: tabular-nums;
542
+ color: var(--muted-foreground);
543
+ }
544
+ }
545
+ </style>
546
+ </template>
547
+ }
548
+
549
+ // ── ToolDiff ─────────────────────────────────────────────────────────────
550
+ // What a write did to one source file, under the row's rows: the file, its
551
+ // +/− counts, a few lines of context around each change, then "Show all".
552
+ // A write never whispers: the change itself is on the row, not behind the
553
+ // disclosure. It is DiffBlock on the row's text column, fed by lineDiff.
554
+
555
+ export interface ToolDiffSignature {
556
+ Args: {
557
+ /** the file as the reader knows it: the path inside the workspace */
558
+ path: string;
559
+ /** the file before the call; absent for a file the call created */
560
+ before?: string;
561
+ /** the file after the call; absent for a file the call deleted */
562
+ after?: string;
563
+ /** a diff already worked out (a result that carries one); used instead
564
+ * of before and after */
565
+ lines?: DiffLine[];
566
+ /** unchanged lines kept on each side of a change. Default 2 */
567
+ context?: number;
568
+ /** lines shown before "Show all". Default 6 */
569
+ maxLines?: number;
570
+ /** DiffBlock's review arguments, passed through: per-hunk Keep and
571
+ * Revert, line comments, the split view, wrapping, the size guard */
572
+ onKeepHunk?: DiffBlockSignature['Args']['onKeepHunk'];
573
+ onRevertHunk?: DiffBlockSignature['Args']['onRevertHunk'];
574
+ hunkStates?: DiffBlockSignature['Args']['hunkStates'];
575
+ comments?: DiffBlockSignature['Args']['comments'];
576
+ onComment?: DiffBlockSignature['Args']['onComment'];
577
+ view?: DiffBlockSignature['Args']['view'];
578
+ wrap?: boolean;
579
+ maxBytes?: number;
580
+ };
581
+ Element: HTMLDivElement;
582
+ }
583
+
584
+ export class ToolDiff extends Component<ToolDiffSignature> {
585
+ get lines(): DiffLine[] {
586
+ return (
587
+ this.args.lines ??
588
+ lineDiff(this.args.before, this.args.after, {
589
+ context: this.args.context ?? 2,
590
+ })
591
+ );
592
+ }
593
+ get receipt(): string | undefined {
594
+ if (this.args.lines) {
595
+ return undefined;
596
+ }
597
+ if (this.args.before === undefined) {
598
+ return 'new file';
599
+ }
600
+ return this.args.after === undefined ? 'deleted' : undefined;
601
+ }
602
+ <template>
603
+ {{#if this.lines.length}}
604
+ <DiffBlock
605
+ class='pretui-tooldiff'
606
+ @lines={{this.lines}}
607
+ @filename={{@path}}
608
+ @maxLines={{if @maxLines @maxLines 6}}
609
+ @expandLabel='Show all'
610
+ @lineNumbers={{true}}
611
+ @receipt={{this.receipt}}
612
+ @onKeepHunk={{@onKeepHunk}}
613
+ @onRevertHunk={{@onRevertHunk}}
614
+ @hunkStates={{@hunkStates}}
615
+ @comments={{@comments}}
616
+ @onComment={{@onComment}}
617
+ @view={{@view}}
618
+ @wrap={{@wrap}}
619
+ @maxBytes={{@maxBytes}}
620
+ data-test-pretui-tool-diff={{@path}}
621
+ ...attributes
622
+ />
623
+ {{/if}}
624
+ <style scoped>
625
+ @layer PretComponent {
626
+ /* the diff sits a step below the row's card, as the design board
627
+ draws it: a darker well with no ring, a quiet file header, and
628
+ the change tints mixed into that well */
629
+ .pretui-tooldiff {
630
+ /* the well follows the theme: its muted surface by default, light
631
+ on a light theme and dark on a dark one, so the code always
632
+ reads in the foreground ink. A theme that wants a deeper well
633
+ sets --pretui-code-well */
634
+ --pretui-tooldiff-well: var(--pretui-code-well, var(--muted));
635
+ margin-block: var(--boxel-sp-6xs) 0;
636
+ min-width: 0;
637
+ overflow-x: auto;
638
+ /* square: inside a row, only the row rounds */
639
+ border-radius: 0;
640
+ background-color: var(--pretui-tooldiff-well);
641
+ box-shadow: none;
642
+ font-size: var(--boxel-font-size-xs);
643
+ line-height: var(--boxel-line-height-xs);
644
+ }
645
+ .pretui-tooldiff :deep(.pretui-diff-head) {
646
+ padding: var(--boxel-sp-4xs) var(--boxel-sp-xs);
647
+ font-size: var(--boxel-caption-font-size);
648
+ line-height: var(--boxel-caption-line-height);
649
+ font-weight: 400;
650
+ color: var(--muted-foreground);
651
+ box-shadow: 0 1px 0
652
+ color-mix(in oklch, var(--border) 60%, transparent);
653
+ }
654
+ .pretui-tooldiff :deep(.pretui-diff-line) {
655
+ padding-inline: var(--boxel-sp-4xs) var(--boxel-sp-xs);
656
+ }
657
+ .pretui-tooldiff :deep(.pretui-diff-line[data-op='add']) {
658
+ /* mixed in sRGB: oklch would carry a teal through blue on its
659
+ way to a purple well, and an added line must read green */
660
+ background-color: color-mix(
661
+ in srgb,
662
+ var(--success) 9%,
663
+ var(--pretui-tooldiff-well)
664
+ );
665
+ }
666
+ .pretui-tooldiff :deep(.pretui-diff-line[data-op='del']) {
667
+ background-color: color-mix(
668
+ in srgb,
669
+ var(--destructive) 11%,
670
+ var(--pretui-tooldiff-well)
671
+ );
672
+ }
673
+ .pretui-tooldiff :deep(.pretui-diff-skip) {
674
+ background: none;
675
+ padding-inline-start: var(--boxel-sp-xs);
676
+ }
677
+ .pretui-tooldiff :deep(.pretui-diff-toggle) {
678
+ padding-inline-start: var(--boxel-sp-xs);
679
+ font-family: var(--font-sans);
680
+ font-size: var(--boxel-caption-font-size);
681
+ line-height: var(--boxel-caption-line-height);
682
+ font-weight: 600;
683
+ }
684
+ .pretui-tooldiff :deep(.pretui-diff-receipt) {
685
+ box-shadow: none;
686
+ padding: var(--boxel-sp-6xs) var(--boxel-sp-xs) var(--boxel-sp-4xs);
687
+ }
688
+ }
689
+ </style>
690
+ </template>
691
+ }
692
+
693
+ // ── ToolReceiptAction ────────────────────────────────────────────────────
694
+ // The one action a receipt still offers: Undo, Revert, Stop. It acts once.
695
+ // While its action runs it is busy and refuses a second press, so an Undo
696
+ // can never be applied twice by an impatient click; the host keeps the
697
+ // durable gate (the call's state), this keeps the instant one. Busy and
698
+ // disabled are aria-disabled, never the disabled attribute, so a pressed
699
+ // button keeps focus.
700
+
701
+ export interface ToolReceiptActionSignature {
702
+ Args: {
703
+ /** the verb: 'Undo', 'Stop', 'Undo 2' */
704
+ label: string;
705
+ /** what it reads while the action runs. Default: the label and "…" */
706
+ busyLabel?: string;
707
+ /** the action; a returned promise keeps the button busy until it
708
+ * settles */
709
+ onAct: () => unknown;
710
+ disabled?: boolean;
711
+ /** the action was refused, and why: "Not undone · file changed since
712
+ * the run". The button gives way to the refusal, in words, in a live
713
+ * region; when the button had focus, the refusal takes it */
714
+ refusal?: string;
715
+ };
716
+ Element: HTMLButtonElement;
717
+ }
718
+
719
+ export class ToolReceiptAction extends Component<ToolReceiptActionSignature> {
720
+ @tracked busy = false;
721
+ /** the button holds focus, or held it when it was removed */
722
+ private hadFocus = false;
723
+ get text(): string {
724
+ return this.busy
725
+ ? (this.args.busyLabel ?? `${this.args.label}…`)
726
+ : this.args.label;
727
+ }
728
+ get inert(): boolean {
729
+ return this.busy || Boolean(this.args.disabled);
730
+ }
731
+ act = async () => {
732
+ if (this.busy || this.args.disabled) {
733
+ return;
734
+ }
735
+ this.busy = true;
736
+ try {
737
+ await this.args.onAct();
738
+ } finally {
739
+ if (!this.isDestroying && !this.isDestroyed) {
740
+ this.busy = false;
741
+ }
742
+ }
743
+ };
744
+ noteFocus = () => {
745
+ this.hadFocus = true;
746
+ };
747
+ // a removed button can blur with no related target: only focus that moved
748
+ // somewhere real clears the flag
749
+ noteBlur = (event: Event) => {
750
+ if ((event as FocusEvent).relatedTarget) {
751
+ this.hadFocus = false;
752
+ }
753
+ };
754
+ /** the refusal takes focus when the button it replaced had it, so a
755
+ * keyboard reader is not left on the page's body */
756
+ takeFocus = modifier((element: HTMLElement) => {
757
+ let active = document.activeElement;
758
+ if (this.hadFocus && (!active || active === document.body)) {
759
+ element.focus({ preventScroll: true });
760
+ }
761
+ this.hadFocus = false;
762
+ });
763
+ <template>
764
+ {{! one live region, present from the first render, whose text changes:
765
+ a region created already holding its text is often not announced }}
766
+ <span class='pretui-toolaction-status' role='status'>{{#if
767
+ @refusal
768
+ }}<span
769
+ class='pretui-toolaction-refusal'
770
+ tabindex='-1'
771
+ {{this.takeFocus}}
772
+ data-test-pretui-tool-receipt-refusal
773
+ >{{@refusal}}</span>{{/if}}</span>
774
+ {{#unless @refusal}}
775
+ <button
776
+ type='button'
777
+ class='pretui-toolaction'
778
+ aria-disabled={{if this.inert 'true'}}
779
+ {{on 'click' this.act}}
780
+ {{on 'focus' this.noteFocus}}
781
+ {{on 'blur' this.noteBlur}}
782
+ data-test-pretui-tool-receipt-action={{@label}}
783
+ ...attributes
784
+ >{{this.text}}</button>
785
+ {{/unless}}
786
+ <style scoped>
787
+ @layer PretComponent {
788
+ /* the receipt's one verb, as text: the settled ink at rest, the
789
+ foreground under the pointer. 1.5rem tall, the smallest target
790
+ a pointer is asked to hit */
791
+ .pretui-toolaction {
792
+ flex: none;
793
+ min-height: 1.5rem;
794
+ margin-inline-end: calc(-1 * var(--boxel-sp-xs));
795
+ padding: 0 var(--boxel-sp-xs);
796
+ border: 0;
797
+ border-radius: var(--boxel-border-radius-xs);
798
+ background-color: transparent;
799
+ font-family: var(--font-sans);
800
+ font-size: var(--boxel-caption-font-size);
801
+ line-height: var(--boxel-caption-line-height);
802
+ font-weight: 600;
803
+ color: var(--pretui-toolrow-settled-ink, var(--muted-foreground));
804
+ cursor: pointer;
805
+ }
806
+ /* a text action says it acts under the pointer in the link ink */
807
+ .pretui-toolaction:hover:not([aria-disabled='true']) {
808
+ color: var(--primary-ink);
809
+ }
810
+ .pretui-toolaction:focus-visible {
811
+ outline: 2px solid var(--ring);
812
+ outline-offset: 1px;
813
+ }
814
+ .pretui-toolaction-refusal {
815
+ font-family: var(--font-sans);
816
+ font-size: var(--boxel-caption-font-size);
817
+ line-height: var(--boxel-caption-line-height);
818
+ color: var(--attention-ink);
819
+ }
820
+ .pretui-toolaction-refusal:focus-visible {
821
+ outline: 2px solid var(--ring);
822
+ outline-offset: 1px;
823
+ }
824
+ .pretui-toolaction[aria-disabled='true'] {
825
+ cursor: default;
826
+ color: var(--subtle-foreground);
827
+ }
828
+ }
829
+ </style>
830
+ </template>
831
+ }
832
+
833
+ // ── The header's contents ────────────────────────────────────────────────
834
+ // Shared by the button form (the row has a body) and the static form (it
835
+ // does not), so the two read identically.
836
+
837
+ interface ToolRowHeadingSignature {
838
+ Args: {
839
+ tone: WorkTone;
840
+ /** the raw state, for the words: a canceled call is struck, a queued
841
+ * one dimmed */
842
+ state: string;
843
+ /** the tool's own icon, the row's one fixed mark */
844
+ icon: ToolIcon;
845
+ verb?: string;
846
+ running: boolean;
847
+ title: string;
848
+ subtitle?: string;
849
+ source?: ToolSource;
850
+ sourceTitle?: string;
851
+ duration?: string;
852
+ status: string;
853
+ chevron: boolean;
854
+ /** the duration stands in for the status word, so it never steps aside */
855
+ timed?: boolean;
856
+ open?: boolean;
857
+ choice?: string;
858
+ /** set in the machine register: the call shows in the title's place */
859
+ call?: string;
860
+ };
861
+ }
862
+
863
+ class ToolRowHeading extends Component<ToolRowHeadingSignature> {
864
+ <template>
865
+ {{! the tool's own icon is the row's mark; only a running call swaps it
866
+ for the thin turning ring. The state is in the words }}
867
+ <span class='pretui-toolrow-disc' data-tone={{@tone}} aria-hidden='true'>
868
+ {{#if @running}}<PlanMark @state='current' @size={{14}} />{{else}}<@icon
869
+ class='pretui-toolrow-icon'
870
+ width='14'
871
+ height='14'
872
+ />{{/if}}
873
+ </span>
874
+ {{#if @verb}}
875
+ <span
876
+ class='pretui-toolrow-verb'
877
+ data-test-pretui-tool-row-verb
878
+ >{{@verb}}</span>
879
+ {{/if}}
880
+ {{#if @call}}
881
+ {{! the machine register: the call itself, in the mono voice }}
882
+ <span
883
+ class='pretui-toolrow-title'
884
+ data-tone={{@tone}}
885
+ data-register='machine'
886
+ data-state={{@state}}
887
+ title={{@call}}
888
+ data-test-pretui-tool-row-title
889
+ >{{@call}}</span>
890
+ {{else}}
891
+ <span
892
+ class='pretui-toolrow-title'
893
+ data-tone={{@tone}}
894
+ data-state={{@state}}
895
+ title={{@title}}
896
+ data-test-pretui-tool-row-title
897
+ >{{@title}}</span>
898
+ {{/if}}
899
+ {{#if @choice}}
900
+ <span
901
+ class='pretui-toolrow-choice'
902
+ data-test-pretui-tool-row-choice
903
+ >{{@choice}}</span>
904
+ {{/if}}
905
+ {{#if @subtitle}}
906
+ <span
907
+ class='pretui-toolrow-subtitle'
908
+ title={{@subtitle}}
909
+ data-test-pretui-tool-row-subtitle
910
+ >{{@subtitle}}</span>
911
+ {{/if}}
912
+ {{#if @source}}
913
+ <span
914
+ class='pretui-toolrow-source'
915
+ title={{@sourceTitle}}
916
+ data-source-kind={{@source.kind}}
917
+ data-test-pretui-tool-row-source
918
+ >
919
+ <Chip @label={{@source.name}} @dot={{false}} />
920
+ </span>
921
+ {{/if}}
922
+ <span
923
+ class='pretui-toolrow-status'
924
+ data-tone={{@tone}}
925
+ data-state={{@state}}
926
+ data-timed={{if @timed 'true'}}
927
+ data-test-pretui-tool-row-status
928
+ >
929
+ {{#if @duration}}<span
930
+ class='pretui-toolrow-duration'
931
+ data-test-pretui-tool-row-duration
932
+ >{{@duration}}</span>{{/if}}
933
+ {{@status}}
934
+ </span>
935
+ {{#if @chevron}}
936
+ <span
937
+ class='pretui-toolrow-chevron'
938
+ data-open={{if @open 'true'}}
939
+ aria-hidden='true'
940
+ ></span>
941
+ {{/if}}
942
+ <style scoped>
943
+ @layer PretComponent {
944
+ /* the verb is no kicker: the row's icon (toolIcon(name, verb))
945
+ already says what kind of call this is, so the word is kept for a
946
+ screen reader only. Density has no label of its own either: it
947
+ is the row's frame (steps plain, the rest boxed). */
948
+ .pretui-toolrow-verb {
949
+ position: absolute;
950
+ width: 1px;
951
+ height: 1px;
952
+ overflow: hidden;
953
+ clip-path: inset(50%);
954
+ white-space: nowrap;
955
+ }
956
+ .pretui-toolrow-disc {
957
+ width: var(--pretui-toolrow-disc, var(--boxel-sp));
958
+ height: var(--pretui-toolrow-disc, var(--boxel-sp));
959
+ display: grid;
960
+ place-items: center;
961
+ flex: none;
962
+ margin-block-start: calc(
963
+ (
964
+ var(--boxel-ui-label-line-height) -
965
+ var(--pretui-toolrow-disc, var(--boxel-sp))
966
+ ) /
967
+ 2
968
+ );
969
+ /* the tool's icon in the quiet ink; a failure or a question tints
970
+ it, and the words beside it say which */
971
+ color: var(--subtle-foreground);
972
+ }
973
+ .pretui-toolrow-icon {
974
+ display: block;
975
+ stroke-width: 1.75;
976
+ }
977
+ .pretui-toolrow-disc[data-tone='failed'] {
978
+ color: var(--destructive-ink);
979
+ }
980
+ .pretui-toolrow-disc[data-tone='attention'] {
981
+ color: var(--attention-ink);
982
+ }
983
+ /* titles can be a whole sentence (an assistant's description of the
984
+ call), so the title shrinks before the status does: the state in
985
+ words must always stay visible */
986
+ .pretui-toolrow-title[data-register='machine'] {
987
+ font-family: var(--font-mono);
988
+ font-size: var(--boxel-font-size-xs);
989
+ line-height: var(--boxel-line-height-xs);
990
+ }
991
+ /* the reader's answer, kept in the settled title as the thing they
992
+ picked, not as prose */
993
+ .pretui-toolrow-choice {
994
+ flex: none;
995
+ display: inline-flex;
996
+ align-items: center;
997
+ max-width: 40%;
998
+ height: 1.125rem;
999
+ padding: 0 var(--boxel-sp-xs);
1000
+ border-radius: var(--boxel-border-radius-sm);
1001
+ background-color: var(--inset);
1002
+ box-shadow: inset 0 0 0 1px var(--border);
1003
+ font-size: var(--boxel-caption-font-size);
1004
+ line-height: var(--boxel-caption-line-height);
1005
+ font-weight: 400;
1006
+ color: var(--foreground);
1007
+ white-space: nowrap;
1008
+ overflow: hidden;
1009
+ text-overflow: ellipsis;
1010
+ }
1011
+ .pretui-toolrow-title {
1012
+ font-size: var(
1013
+ --pretui-toolrow-title-size,
1014
+ var(--boxel-ui-label-font-size)
1015
+ );
1016
+ line-height: var(--boxel-ui-label-line-height);
1017
+ font-weight: 600;
1018
+ flex: 0 1 auto;
1019
+ min-width: 3em;
1020
+ /* one line: a column of calls stays short, and a long title runs
1021
+ off its end (whole on hover, and wrapped once the row is open) */
1022
+ white-space: nowrap;
1023
+ overflow: hidden;
1024
+ text-overflow: ellipsis;
1025
+ }
1026
+ /* settled work says so in a quieter ink, never by fading: a screen
1027
+ of history must stay readable */
1028
+ .pretui-toolrow-title[data-tone='done'],
1029
+ .pretui-toolrow-title[data-tone='idle'] {
1030
+ font-weight: 400;
1031
+ color: var(--pretui-toolrow-settled-ink, var(--muted-foreground));
1032
+ }
1033
+ /* the state in the words: a running call's title shimmers, a queued
1034
+ one waits dimmed, a canceled or superseded one is struck */
1035
+ .pretui-toolrow-title[data-tone='running'] {
1036
+ background: linear-gradient(
1037
+ 90deg,
1038
+ var(--foreground) 0%,
1039
+ var(--foreground) 40%,
1040
+ color-mix(in oklch, var(--foreground) 45%, transparent) 50%,
1041
+ var(--foreground) 60%,
1042
+ var(--foreground) 100%
1043
+ )
1044
+ 0 0 / 300% 100%;
1045
+ -webkit-background-clip: text;
1046
+ background-clip: text;
1047
+ color: transparent;
1048
+ animation: pretui-toolrow-shimmer 2.4s linear infinite;
1049
+ }
1050
+ @keyframes pretui-toolrow-shimmer {
1051
+ from {
1052
+ background-position: 100% 0;
1053
+ }
1054
+ to {
1055
+ background-position: 0% 0;
1056
+ }
1057
+ }
1058
+ @media (prefers-reduced-motion: reduce) {
1059
+ .pretui-toolrow-title[data-tone='running'] {
1060
+ animation: none;
1061
+ background: none;
1062
+ color: var(--foreground);
1063
+ }
1064
+ }
1065
+ /* queued: the quietest ink, never faded with opacity */
1066
+ .pretui-toolrow-title[data-state='pending'] {
1067
+ color: var(--subtle-foreground);
1068
+ }
1069
+ .pretui-toolrow-title[data-state='canceled'],
1070
+ .pretui-toolrow-title[data-state='superseded'],
1071
+ .pretui-toolrow-title[data-state='reverted'] {
1072
+ text-decoration: line-through;
1073
+ text-decoration-color: color-mix(
1074
+ in oklch,
1075
+ currentColor 55%,
1076
+ transparent
1077
+ );
1078
+ }
1079
+ .pretui-toolrow-subtitle {
1080
+ min-width: 0;
1081
+ flex: 0 1 auto;
1082
+ font-family: var(--font-mono);
1083
+ font-size: var(--boxel-font-size-xs);
1084
+ line-height: var(--boxel-line-height-xs);
1085
+ color: var(--muted-foreground);
1086
+ white-space: nowrap;
1087
+ overflow: hidden;
1088
+ text-overflow: ellipsis;
1089
+ }
1090
+ .pretui-toolrow-source {
1091
+ flex: none;
1092
+ --pretui-chip-font-size: var(--boxel-caption-font-size);
1093
+ }
1094
+ .pretui-toolrow-status {
1095
+ margin-inline-start: auto;
1096
+ /* a caption line centred on the title's first line */
1097
+ margin-block-start: 1px;
1098
+ flex: none;
1099
+ display: flex;
1100
+ align-items: center;
1101
+ gap: var(--boxel-sp-xs);
1102
+ font-size: var(--boxel-caption-font-size);
1103
+ line-height: var(--boxel-caption-line-height);
1104
+ color: var(--subtle-foreground);
1105
+ font-variant-numeric: tabular-nums;
1106
+ }
1107
+ /* a state colours the word, never the row */
1108
+ .pretui-toolrow-status[data-tone='attention'] {
1109
+ color: var(--attention-ink);
1110
+ font-weight: 600;
1111
+ }
1112
+ .pretui-toolrow-status[data-tone='failed'] {
1113
+ color: var(--destructive-ink);
1114
+ font-weight: 600;
1115
+ }
1116
+ .pretui-toolrow-duration {
1117
+ color: var(--muted-foreground);
1118
+ font-weight: 400;
1119
+ }
1120
+ .pretui-toolrow-chevron {
1121
+ flex: none;
1122
+ width: 0.375rem;
1123
+ height: 0.375rem;
1124
+ margin-block-start: 0.375rem;
1125
+ border-inline-end: 1.5px solid var(--subtle-foreground);
1126
+ border-block-end: 1.5px solid var(--subtle-foreground);
1127
+ transform: rotate(-45deg);
1128
+ transition: transform 160ms
1129
+ var(--pretui-ease-enter, cubic-bezier(0.22, 0.61, 0.25, 1));
1130
+ }
1131
+ .pretui-toolrow-chevron[data-open] {
1132
+ transform: rotate(45deg);
1133
+ }
1134
+ @media (prefers-reduced-motion: reduce) {
1135
+ .pretui-toolrow-chevron {
1136
+ transition: none;
1137
+ }
1138
+ }
1139
+ }
1140
+ </style>
1141
+ </template>
1142
+ }
1143
+
1144
+ // ── ToolRow ──────────────────────────────────────────────────────────────
1145
+
1146
+ export interface ToolRowSignature {
1147
+ Args: {
1148
+ /** the tool's name, e.g. 'patch-fields', 'read_realm_file'. It picks
1149
+ * the row's icon (toolIcon) unless `@icon` does */
1150
+ name: string;
1151
+ /** the row's mark: a boxel-icons component. Default: toolIcon(name, verb) */
1152
+ icon?: ToolIcon;
1153
+ /** WorkItem's open state vocabulary, plus `streaming` while the
1154
+ * arguments are still arriving */
1155
+ state?: string;
1156
+ /** default: the renderer's title, else the humanized name */
1157
+ title?: string;
1158
+ /** what the tool acted on: the file, the command, the query. Default:
1159
+ * the renderer's subtitle(params) */
1160
+ subtitle?: string;
1161
+ /** which skill (or the platform) granted this tool */
1162
+ source?: ToolSource;
1163
+ /** overrides the state's default phrase, as in WorkItem */
1164
+ status?: string;
1165
+ /** seconds the call took; shown once it settles */
1166
+ duration?: number;
1167
+ /** the failure; its first line shows under the row, the whole of it in
1168
+ * the body */
1169
+ error?: string;
1170
+ /** the call's arguments (raw text while streaming) and result, handed
1171
+ * to the body. `@params` because `@args` is reserved in templates. */
1172
+ params?: unknown;
1173
+ result?: unknown;
1174
+ renderers?: ToolRenderers;
1175
+ /** controlled disclosure */
1176
+ open?: boolean;
1177
+ /** uncontrolled initial disclosure. Default: open when the call failed
1178
+ * or needs the reader, closed otherwise */
1179
+ defaultOpen?: boolean;
1180
+ /** fires when the reader toggles the row */
1181
+ onOpenChange?: (open: boolean) => void;
1182
+ /** mount the body through the shared bottom-first queue. Default true */
1183
+ lazy?: boolean;
1184
+ /** CSS length the body reserves while it waits to mount. Default 0 */
1185
+ reserve?: string;
1186
+ /** whether settled work recedes. Default: a done, canceled or reverted
1187
+ * call recedes; a running, failed or needs-you call never does. It only
1188
+ * shows inside a container that sets --pretui-recede-opacity */
1189
+ recede?: boolean;
1190
+ /** how much of the transcript the call claims. It sets `data-density`;
1191
+ * the row says it through its frame (@framed), never with a label */
1192
+ density?: ToolDensity;
1193
+ /** a fixed verb before the title: READ, UPDATE, RESEARCH. The verb never
1194
+ * moves; only the title after it changes */
1195
+ verb?: string;
1196
+ /** a multi-step call's position; shows under the header while the call
1197
+ * is not settled */
1198
+ progress?: ToolProgress;
1199
+ /** draw the row as a box: for actions, runs and scripts, whose rows,
1200
+ * gate, result and receipt belong together. Steps stay unframed */
1201
+ framed?: boolean;
1202
+ /** a closing line: run id, preview hash, bytes read. Its action goes in
1203
+ * the `receipt` block */
1204
+ receipt?: string;
1205
+ /** answer the call from its result renderer (`renderer.result`); the
1206
+ * row hands it on only while the call needs the reader */
1207
+ resolve?: (value: unknown) => unknown;
1208
+ /** what the reader picked, kept in the settled title as a pill:
1209
+ * "Chose the layout [Compact]" */
1210
+ choice?: string;
1211
+ /** the call as the machine wrote it: `read_file("plan.gts")` */
1212
+ call?: string;
1213
+ /** 'editorial' (default) titles the row in words; 'machine' shows
1214
+ * @call in the title's place, for a reader who wants the calls */
1215
+ register?: 'editorial' | 'machine';
1216
+ };
1217
+ Blocks: {
1218
+ body: [];
1219
+ actions: [];
1220
+ error: [];
1221
+ /** lines on the rail under the header, always visible: what happened.
1222
+ * Use ToolLine */
1223
+ rows: [];
1224
+ /** the decision this call waits on, inside the row */
1225
+ gate: [];
1226
+ /** the final thing the call produced, such as the card it made */
1227
+ result: [];
1228
+ /** the one action the receipt still offers: Undo, Stop */
1229
+ receipt: [];
1230
+ };
1231
+ Element: HTMLDivElement;
1232
+ }
1233
+
1234
+ /** Calls back on install and whenever the value changes. */
1235
+ const watch = modifier(
1236
+ (
1237
+ _el: HTMLElement,
1238
+ [callback, value]: [(value: unknown) => void, unknown],
1239
+ ) => {
1240
+ callback(value);
1241
+ },
1242
+ );
1243
+
1244
+ /**
1245
+ * One tool call.
1246
+ *
1247
+ * ```hbs
1248
+ * <ToolRow @name='patch-fields' @subtitle='Model/base-plate' @state={{call.state}}
1249
+ * @source={{hash kind='skill' name='3d-modeling'}} @duration={{call.seconds}}>
1250
+ * <:body><FieldDiff @changes={{call.result.changes}} /></:body>
1251
+ * <:actions><Button @size='s'>Apply</Button></:actions>
1252
+ * </ToolRow>
1253
+ * ```
1254
+ *
1255
+ * Better than WorkItem for this job, on the gaps WorkItem's own write-up
1256
+ * lists: state changes into "needs you" and "failed" are announced through a
1257
+ * live region present from the first render, the glyph is aria-hidden, the
1258
+ * row is a labelled group, and the disclosure is a real button.
1259
+ */
1260
+ export class ToolRow extends Component<ToolRowSignature> {
1261
+ /** the reader's choice; once set, @defaultOpen no longer moves the row */
1262
+ @tracked private userOpen: boolean | undefined;
1263
+ @tracked mounted = false;
1264
+ private cancelMount: (() => void) | undefined;
1265
+ readonly bodyId = guidFor(this) + '-body';
1266
+
1267
+ get state(): string {
1268
+ return this.args.state ?? 'pending';
1269
+ }
1270
+ get machineCall(): string | undefined {
1271
+ return this.args.register === 'machine' ? this.args.call : undefined;
1272
+ }
1273
+ get isStreaming(): boolean {
1274
+ return this.state === 'streaming';
1275
+ }
1276
+ get tone(): WorkTone {
1277
+ return this.isStreaming ? 'running' : workTone(this.state);
1278
+ }
1279
+ get icon(): ToolIcon {
1280
+ return this.args.icon ?? toolIcon(this.args.name, this.args.verb);
1281
+ }
1282
+ get isRunning(): boolean {
1283
+ return this.tone === 'running';
1284
+ }
1285
+ /** a completed call whose time is its whole state */
1286
+ get isTimed(): boolean {
1287
+ return (
1288
+ !this.args.status && this.state === 'completed' && !!this.durationText
1289
+ );
1290
+ }
1291
+ get statusText(): string {
1292
+ if (this.args.status) {
1293
+ return this.args.status;
1294
+ }
1295
+ // a completed call with a time says only the time: its title in the
1296
+ // settled ink already reads as finished. The other done states (kept,
1297
+ // delivered, partly kept) say more than "finished", so their word stays
1298
+ if (this.isTimed) {
1299
+ return '';
1300
+ }
1301
+ return this.isStreaming ? 'preparing…' : workStatus(this.state);
1302
+ }
1303
+ get renderer(): ToolRenderer | undefined {
1304
+ return this.args.renderers?.[this.args.name];
1305
+ }
1306
+ get title(): string {
1307
+ return (
1308
+ this.args.title ??
1309
+ this.renderer?.title ??
1310
+ humanizeToolName(this.args.name)
1311
+ );
1312
+ }
1313
+ get subtitle(): string | undefined {
1314
+ if (this.args.subtitle !== undefined) {
1315
+ return this.args.subtitle;
1316
+ }
1317
+ if (this.isStreaming || !this.renderer?.subtitle) {
1318
+ return undefined;
1319
+ }
1320
+ try {
1321
+ return this.renderer.subtitle(this.args.params);
1322
+ } catch {
1323
+ return undefined;
1324
+ }
1325
+ }
1326
+ get rendererResult(): ComponentLike<ToolResultSignature> | undefined {
1327
+ return this.renderer?.result;
1328
+ }
1329
+ /** the resolver, only while the call waits on the reader */
1330
+ get resolver(): ((value: unknown) => unknown) | undefined {
1331
+ return this.tone === 'attention' ? this.args.resolve : undefined;
1332
+ }
1333
+ // When the gate's controls leave (the decision was made) while focus is
1334
+ // in them, focus would fall to the page; it goes to the row's header
1335
+ // instead, so a keyboard reader stays where they were. Runtime lookups use
1336
+ // the data-toolrow attributes, never the styling classes.
1337
+ watchGate = modifier((element: HTMLElement) => {
1338
+ // the control in the gate that last had focus; cleared once focus
1339
+ // moves somewhere real outside the gate, or once it is handed on
1340
+ let held: Element | undefined;
1341
+ // whether the reader reached the gate by keyboard: only then does the
1342
+ // header show its ring when focus lands on it
1343
+ let keyboard = false;
1344
+ let onFocusIn = (event: FocusEvent) => {
1345
+ held = event.target as Element;
1346
+ keyboard = held.matches(':focus-visible');
1347
+ };
1348
+ // focus that moves somewhere real outside the gate clears `held`. A
1349
+ // blur with no related target is either a removed control (the
1350
+ // decision) or the reader leaving for blank page space or another
1351
+ // window; only the second leaves the control connected, and then the
1352
+ // reader has moved on, so a later change in the gate moves no focus
1353
+ let onFocusOut = (event: FocusEvent) => {
1354
+ let to = event.relatedTarget as Node | null;
1355
+ if (to) {
1356
+ if (!element.contains(to)) {
1357
+ held = undefined;
1358
+ }
1359
+ return;
1360
+ }
1361
+ let left = event.target as Element;
1362
+ queueMicrotask(() => {
1363
+ if (held === left && left.isConnected) {
1364
+ held = undefined;
1365
+ }
1366
+ });
1367
+ };
1368
+ let observer = new MutationObserver(() => {
1369
+ if (!held || held.isConnected) {
1370
+ return;
1371
+ }
1372
+ held = undefined;
1373
+ let active = document.activeElement;
1374
+ if (active && active !== document.body) {
1375
+ // focus already went somewhere on purpose
1376
+ return;
1377
+ }
1378
+ let header = element
1379
+ .closest('[data-toolrow]')
1380
+ ?.querySelector<HTMLElement>('[data-toolrow-header]');
1381
+ if (header) {
1382
+ if (header.tagName !== 'BUTTON' && !header.hasAttribute('tabindex')) {
1383
+ header.setAttribute('tabindex', '-1');
1384
+ }
1385
+ if (!keyboard) {
1386
+ header.setAttribute('data-quiet-focus', '');
1387
+ header.addEventListener(
1388
+ 'blur',
1389
+ () => header.removeAttribute('data-quiet-focus'),
1390
+ { once: true },
1391
+ );
1392
+ }
1393
+ header.focus({ preventScroll: true });
1394
+ }
1395
+ });
1396
+ observer.observe(element, { childList: true, subtree: true });
1397
+ element.addEventListener('focusin', onFocusIn);
1398
+ element.addEventListener('focusout', onFocusOut);
1399
+ return () => {
1400
+ observer.disconnect();
1401
+ element.removeEventListener('focusin', onFocusIn);
1402
+ element.removeEventListener('focusout', onFocusOut);
1403
+ };
1404
+ });
1405
+ get rendererBody(): ComponentLike<ToolBodySignature> | undefined {
1406
+ return this.renderer?.body;
1407
+ }
1408
+ get hasDefaultBody(): boolean {
1409
+ return (
1410
+ this.args.params !== undefined ||
1411
+ this.args.result !== undefined ||
1412
+ Boolean(this.args.error)
1413
+ );
1414
+ }
1415
+ get errorFirstLine(): string | undefined {
1416
+ return this.args.error?.split('\n', 1)[0];
1417
+ }
1418
+ get durationText(): string | undefined {
1419
+ if (
1420
+ this.args.duration === undefined ||
1421
+ (this.tone !== 'done' && this.tone !== 'failed')
1422
+ ) {
1423
+ return undefined;
1424
+ }
1425
+ return formatToolDuration(this.args.duration);
1426
+ }
1427
+ get sourceTitle(): string | undefined {
1428
+ let source = this.args.source;
1429
+ if (!source) {
1430
+ return undefined;
1431
+ }
1432
+ return source.kind === 'skill'
1433
+ ? `Granted by the ${source.name} skill`
1434
+ : `Built-in tool: ${source.name}`;
1435
+ }
1436
+ get defaultOpen(): boolean {
1437
+ return (
1438
+ this.args.defaultOpen ??
1439
+ (this.tone === 'failed' || this.tone === 'attention')
1440
+ );
1441
+ }
1442
+ get isOpen(): boolean {
1443
+ return this.args.open ?? this.userOpen ?? this.defaultOpen;
1444
+ }
1445
+ get recedes(): boolean {
1446
+ if (this.args.recede !== undefined) {
1447
+ return this.args.recede;
1448
+ }
1449
+ // a run reconnecting after a reload is still live work, only unseen
1450
+ return (
1451
+ this.tone === 'done' ||
1452
+ (this.tone === 'idle' && this.state !== 'reconnecting')
1453
+ );
1454
+ }
1455
+ get lazy(): boolean {
1456
+ return this.args.lazy ?? true;
1457
+ }
1458
+ get showBody(): boolean {
1459
+ return !this.lazy || this.mounted;
1460
+ }
1461
+ get reserveStyle(): SafeString | undefined {
1462
+ return this.args.reserve
1463
+ ? htmlSafe(`min-height: ${this.args.reserve}`)
1464
+ : undefined;
1465
+ }
1466
+ /** What the live region says. Only the two transitions a reader must hear
1467
+ * about are announced: the call now needs them, or it failed. */
1468
+ get announcement(): string {
1469
+ if (this.tone === 'attention') {
1470
+ return `${this.title}: needs you`;
1471
+ }
1472
+ if (this.tone === 'failed') {
1473
+ return `${this.title}: failed`;
1474
+ }
1475
+ return '';
1476
+ }
1477
+ /** Polite, so a group whose calls fail together does not interrupt the
1478
+ * reader once per row; only a call that now needs them is assertive. */
1479
+ get liveness(): 'assertive' | 'polite' {
1480
+ return this.tone === 'attention' ? 'assertive' : 'polite';
1481
+ }
1482
+
1483
+ /** Progress belongs to work in flight; a settled call says what it did in
1484
+ * its title and rows instead. */
1485
+ get showProgress(): boolean {
1486
+ return (
1487
+ Boolean(this.args.progress) &&
1488
+ this.tone !== 'done' &&
1489
+ this.tone !== 'failed' &&
1490
+ this.tone !== 'idle'
1491
+ );
1492
+ }
1493
+ get progressLabel(): string {
1494
+ return this.args.progress ? progressText(this.args.progress) : '';
1495
+ }
1496
+ get progressFill(): SafeString {
1497
+ let percent = this.args.progress ? progressPercent(this.args.progress) : 0;
1498
+ return htmlSafe(`width: ${percent}%`);
1499
+ }
1500
+ get progressValue(): number {
1501
+ return this.args.progress ? progressPercent(this.args.progress) : 0;
1502
+ }
1503
+
1504
+ toggle = () => {
1505
+ let next = !this.isOpen;
1506
+ if (this.args.open === undefined) {
1507
+ this.userOpen = next;
1508
+ }
1509
+ this.args.onOpenChange?.(next);
1510
+ };
1511
+
1512
+ /** Runs on install and on every change of the effective open state. */
1513
+ openChanged = (value: unknown) => {
1514
+ if (!value) {
1515
+ // closed before its turn came: it never mounts
1516
+ this.cancelMount?.();
1517
+ this.cancelMount = undefined;
1518
+ return;
1519
+ }
1520
+ if (!this.lazy || this.mounted || this.cancelMount) {
1521
+ return;
1522
+ }
1523
+ this.cancelMount = scheduleMount(() => {
1524
+ this.cancelMount = undefined;
1525
+ if (!this.isDestroying && !this.isDestroyed) {
1526
+ this.mounted = true;
1527
+ }
1528
+ });
1529
+ };
1530
+
1531
+ willDestroy() {
1532
+ super.willDestroy();
1533
+ this.cancelMount?.();
1534
+ }
1535
+
1536
+ <template>
1537
+ <div
1538
+ class='pretui-toolrow'
1539
+ role='group'
1540
+ data-toolrow
1541
+ aria-label={{this.title}}
1542
+ data-state={{this.state}}
1543
+ data-tone={{this.tone}}
1544
+ data-open={{if this.isOpen 'true' 'false'}}
1545
+ data-mounted={{if this.showBody 'true' 'false'}}
1546
+ data-recede={{if this.recedes 'true'}}
1547
+ data-framed={{if @framed 'true'}}
1548
+ data-density={{@density}}
1549
+ data-test-pretui-tool-row
1550
+ ...attributes
1551
+ >
1552
+ {{#let
1553
+ (either (has-block 'body') this.rendererBody this.hasDefaultBody)
1554
+ as |hasBody|
1555
+ }}
1556
+ <div class='pretui-toolrow-row' {{watch this.openChanged this.isOpen}}>
1557
+ {{#if hasBody}}
1558
+ <button
1559
+ type='button'
1560
+ class='pretui-toolrow-header'
1561
+ aria-expanded={{if this.isOpen 'true' 'false'}}
1562
+ aria-controls={{this.bodyId}}
1563
+ data-toolrow-header
1564
+ {{on 'click' this.toggle}}
1565
+ data-test-pretui-tool-row-toggle
1566
+ >
1567
+ <ToolRowHeading
1568
+ @tone={{this.tone}}
1569
+ @state={{this.state}}
1570
+ @icon={{this.icon}}
1571
+ @verb={{@verb}}
1572
+ @running={{this.isRunning}}
1573
+ @title={{this.title}}
1574
+ @subtitle={{this.subtitle}}
1575
+ @source={{@source}}
1576
+ @sourceTitle={{this.sourceTitle}}
1577
+ @duration={{this.durationText}}
1578
+ @status={{this.statusText}}
1579
+ @timed={{this.isTimed}}
1580
+ @chevron={{true}}
1581
+ @open={{this.isOpen}}
1582
+ @choice={{@choice}}
1583
+ @call={{this.machineCall}}
1584
+ />
1585
+ </button>
1586
+ {{else}}
1587
+ <div class='pretui-toolrow-header' data-toolrow-header>
1588
+ <ToolRowHeading
1589
+ @tone={{this.tone}}
1590
+ @state={{this.state}}
1591
+ @icon={{this.icon}}
1592
+ @verb={{@verb}}
1593
+ @running={{this.isRunning}}
1594
+ @title={{this.title}}
1595
+ @subtitle={{this.subtitle}}
1596
+ @source={{@source}}
1597
+ @sourceTitle={{this.sourceTitle}}
1598
+ @duration={{this.durationText}}
1599
+ @status={{this.statusText}}
1600
+ @timed={{this.isTimed}}
1601
+ @chevron={{false}}
1602
+ @choice={{@choice}}
1603
+ @call={{this.machineCall}}
1604
+ />
1605
+ </div>
1606
+ {{/if}}
1607
+ {{#if (has-block 'actions')}}
1608
+ <div
1609
+ class='pretui-toolrow-actions'
1610
+ data-test-pretui-tool-row-actions
1611
+ >
1612
+ {{yield to='actions'}}
1613
+ </div>
1614
+ {{/if}}
1615
+ </div>
1616
+ {{#if this.showProgress}}
1617
+ <div
1618
+ class='pretui-toolrow-progress'
1619
+ data-test-pretui-tool-row-progress
1620
+ >
1621
+ <span
1622
+ class='pretui-toolrow-progress-label'
1623
+ >{{this.progressLabel}}</span>
1624
+ <span
1625
+ class='pretui-toolrow-progress-track'
1626
+ role='progressbar'
1627
+ aria-label={{this.progressLabel}}
1628
+ aria-valuemin='0'
1629
+ aria-valuemax='100'
1630
+ aria-valuenow={{this.progressValue}}
1631
+ ><span
1632
+ class='pretui-toolrow-progress-fill'
1633
+ style={{this.progressFill}}
1634
+ ></span></span>
1635
+ </div>
1636
+ {{/if}}
1637
+ {{#if (has-block 'rows')}}
1638
+ <div class='pretui-toolrow-rows' data-test-pretui-tool-row-rows>
1639
+ {{yield to='rows'}}
1640
+ </div>
1641
+ {{/if}}
1642
+ {{#if this.errorFirstLine}}
1643
+ {{#unless this.isOpen}}
1644
+ <div
1645
+ class='pretui-toolrow-errorline'
1646
+ data-test-pretui-tool-row-error-line
1647
+ >
1648
+ {{this.errorFirstLine}}
1649
+ </div>
1650
+ {{/unless}}
1651
+ {{/if}}
1652
+ {{#if hasBody}}
1653
+ <Collapse @open={{this.isOpen}} id={{this.bodyId}}>
1654
+ <div class='pretui-toolrow-body' data-test-pretui-tool-row-body>
1655
+ {{#if this.showBody}}
1656
+ {{#if @error}}
1657
+ {{#if (has-block 'error')}}
1658
+ {{yield to='error'}}
1659
+ {{else}}
1660
+ <pre
1661
+ class='pretui-toolrow-error'
1662
+ data-test-pretui-tool-row-error
1663
+ >{{@error}}</pre>
1664
+ {{/if}}
1665
+ {{/if}}
1666
+ {{#if (has-block 'body')}}
1667
+ {{yield to='body'}}
1668
+ {{else if this.rendererBody}}
1669
+ <this.rendererBody
1670
+ @params={{@params}}
1671
+ @result={{@result}}
1672
+ @state={{this.state}}
1673
+ />
1674
+ {{else}}
1675
+ <GenericToolBody
1676
+ @params={{@params}}
1677
+ @result={{@result}}
1678
+ @state={{this.state}}
1679
+ />
1680
+ {{/if}}
1681
+ {{else}}
1682
+ <div
1683
+ class='pretui-toolrow-reserve'
1684
+ style={{this.reserveStyle}}
1685
+ aria-hidden='true'
1686
+ data-test-pretui-tool-row-reserve
1687
+ ></div>
1688
+ {{/if}}
1689
+ </div>
1690
+ </Collapse>
1691
+ {{/if}}
1692
+ {{/let}}
1693
+ {{#if (has-block 'gate')}}
1694
+ <div
1695
+ class='pretui-toolrow-gate'
1696
+ {{this.watchGate}}
1697
+ data-test-pretui-tool-row-gate
1698
+ >
1699
+ {{yield to='gate'}}
1700
+ </div>
1701
+ {{/if}}
1702
+ {{#if (has-block 'result')}}
1703
+ <div class='pretui-toolrow-result' data-test-pretui-tool-row-result>
1704
+ {{yield to='result'}}
1705
+ </div>
1706
+ {{else if this.rendererResult}}
1707
+ <div class='pretui-toolrow-result' data-test-pretui-tool-row-result>
1708
+ <this.rendererResult
1709
+ @params={{@params}}
1710
+ @result={{@result}}
1711
+ @state={{this.state}}
1712
+ @resolve={{this.resolver}}
1713
+ />
1714
+ </div>
1715
+ {{/if}}
1716
+ {{#if (either @receipt (has-block 'receipt'))}}
1717
+ <div class='pretui-toolrow-receipt' data-test-pretui-tool-row-receipt>
1718
+ <span
1719
+ class='pretui-toolrow-receipt-text'
1720
+ data-test-pretui-tool-row-receipt-text
1721
+ >{{@receipt}}</span>
1722
+ {{#if (has-block 'receipt')}}
1723
+ <span class='pretui-toolrow-receipt-action'>{{yield
1724
+ to='receipt'
1725
+ }}</span>
1726
+ {{/if}}
1727
+ </div>
1728
+ {{/if}}
1729
+ <span
1730
+ class='pretui-toolrow-live'
1731
+ role='status'
1732
+ aria-live={{this.liveness}}
1733
+ data-test-pretui-tool-row-live
1734
+ >{{this.announcement}}</span>
1735
+ </div>
1736
+ <style scoped>
1737
+ @layer PretComponent {
1738
+ /* ── one grid ─────────────────────────────────────────────────
1739
+ Every part of a row hangs off three numbers: the inset, the
1740
+ disc, and the gap after it. Everything under the header starts
1741
+ at the text column (inset + disc + gap), so the progress line,
1742
+ the rows, the body, the gate, the result and the receipt all
1743
+ share one left edge with the title, and a nested ToolLine puts
1744
+ its own glyph on that edge. Framed rows widen the inset and
1745
+ nothing else, so the parts recombine the same way in both. */
1746
+ /* every row, framed or not, hangs its inset outside the text edge:
1747
+ its disc sits in the transcript's one mark column (with the
1748
+ speaker's initial, the system lines' icons and the fold row's
1749
+ check) and its title on the one title edge, so a column of steps
1750
+ and boxed calls never zig-zags. A frame is drawn in the gutter */
1751
+ .pretui-toolrow {
1752
+ margin-inline: calc(-1 * var(--pretui-toolrow-inset));
1753
+ }
1754
+ .pretui-toolrow {
1755
+ --pretui-toolrow-inset: var(--boxel-sp-xs);
1756
+ --pretui-toolrow-disc: var(--boxel-sp);
1757
+ --pretui-toolrow-gap: var(--boxel-sp-xs);
1758
+ --pretui-toolrow-text: calc(
1759
+ var(--pretui-toolrow-inset) + var(--pretui-toolrow-disc) +
1760
+ var(--pretui-toolrow-gap)
1761
+ );
1762
+ display: flex;
1763
+ flex-direction: column;
1764
+ font-size: var(--boxel-ui-label-font-size);
1765
+ line-height: var(--boxel-ui-label-line-height);
1766
+ border-radius: var(--boxel-border-radius-sm);
1767
+ /* one rounded box per row: it clips what meets its edge (the
1768
+ header's hover, the receipt's rule), and nothing inside it
1769
+ rounds again */
1770
+ overflow: clip;
1771
+ /* the row answers its own width, not the window's: the same row
1772
+ in a 320px side panel and a 680px one reads right in both */
1773
+ container-type: inline-size;
1774
+ container-name: pretui-toolrow;
1775
+ /* off-screen rows skip layout and paint; the browser remembers each
1776
+ row's last size, so the scrollbar does not jump */
1777
+ content-visibility: auto;
1778
+ contain-intrinsic-size: auto 2rem;
1779
+ }
1780
+ /* the prominence ramp: settled work recedes, the live frontier and
1781
+ anything needing the reader stay at full strength. Off unless a
1782
+ container sets the token; hover or focus brings a row back. */
1783
+ .pretui-toolrow-row {
1784
+ transition: opacity 200ms linear;
1785
+ }
1786
+ .pretui-toolrow[data-recede] > .pretui-toolrow-row {
1787
+ opacity: var(--pretui-recede-opacity, 1);
1788
+ }
1789
+ .pretui-toolrow[data-recede]:is(:hover, :focus-within)
1790
+ > .pretui-toolrow-row {
1791
+ opacity: 1;
1792
+ }
1793
+ /* an open row has room to say all of it, and a row that needs the
1794
+ reader says all of what it asks: its title wraps */
1795
+ .pretui-toolrow:is([data-open='true'], [data-tone='attention'])
1796
+ > .pretui-toolrow-row
1797
+ :deep(.pretui-toolrow-title) {
1798
+ white-space: normal;
1799
+ overflow-wrap: anywhere;
1800
+ }
1801
+ .pretui-toolrow-row {
1802
+ display: flex;
1803
+ align-items: center;
1804
+ gap: var(--boxel-sp-4xs);
1805
+ min-width: 0;
1806
+ }
1807
+ /* everything in the header sits on the title's first line, so a
1808
+ title that wraps keeps its icon, status and chevron level with
1809
+ the words they belong to: the block padding plus one label
1810
+ line is the 2rem row */
1811
+ .pretui-toolrow-header {
1812
+ flex: 1;
1813
+ min-width: 0;
1814
+ display: flex;
1815
+ align-items: flex-start;
1816
+ gap: var(--pretui-toolrow-gap);
1817
+ padding: 0.4375rem var(--pretui-toolrow-inset);
1818
+ min-height: 2rem;
1819
+ box-sizing: border-box;
1820
+ border: 0;
1821
+ /* square: the row is the one box that rounds, and clips this */
1822
+ border-radius: 0;
1823
+ background-color: transparent;
1824
+ color: inherit;
1825
+ font-family: inherit;
1826
+ font-size: inherit;
1827
+ font-weight: inherit;
1828
+ line-height: inherit;
1829
+ text-align: start;
1830
+ }
1831
+ button.pretui-toolrow-header {
1832
+ cursor: pointer;
1833
+ }
1834
+ button.pretui-toolrow-header:hover {
1835
+ background-color: var(--inset);
1836
+ }
1837
+ /* both header forms: the static one takes focus when a gate hands
1838
+ it back on a row with no body */
1839
+ .pretui-toolrow-header:focus-visible {
1840
+ outline: 2px solid var(--ring);
1841
+ outline-offset: -2px;
1842
+ }
1843
+ /* focus handed over after a click on the gate: kept for the next
1844
+ Tab, but drawn only for a keyboard reader */
1845
+ .pretui-toolrow-header[data-quiet-focus]:focus-visible {
1846
+ outline: none;
1847
+ }
1848
+ .pretui-toolrow-actions {
1849
+ display: flex;
1850
+ align-items: center;
1851
+ gap: var(--boxel-sp-4xs);
1852
+ flex: none;
1853
+ padding-inline-end: var(--pretui-toolrow-inset);
1854
+ }
1855
+ /* a failure's first line reads in ink: the status word and the
1856
+ mark carry the state, never red text */
1857
+ .pretui-toolrow-errorline {
1858
+ padding: 0 var(--pretui-toolrow-inset) var(--boxel-sp-xs)
1859
+ var(--pretui-toolrow-text);
1860
+ font-size: var(--boxel-ui-label-font-size);
1861
+ line-height: var(--boxel-ui-label-line-height);
1862
+ color: var(--foreground);
1863
+ white-space: nowrap;
1864
+ overflow: hidden;
1865
+ text-overflow: ellipsis;
1866
+ }
1867
+ .pretui-toolrow-body {
1868
+ display: flex;
1869
+ flex-direction: column;
1870
+ gap: var(--boxel-sp-xs);
1871
+ padding: var(--boxel-sp-6xs) var(--pretui-toolrow-inset)
1872
+ var(--boxel-sp-xs) var(--pretui-toolrow-text);
1873
+ min-width: 0;
1874
+ }
1875
+ /* the sections under the header, in reading order. Each owns its
1876
+ bottom spacing, so any subset stacks evenly. */
1877
+ .pretui-toolrow-progress {
1878
+ display: flex;
1879
+ flex-direction: column;
1880
+ gap: var(--boxel-sp-4xs);
1881
+ padding: 0 var(--pretui-toolrow-inset) var(--boxel-sp-xs)
1882
+ var(--pretui-toolrow-text);
1883
+ }
1884
+ .pretui-toolrow-progress-label {
1885
+ font-size: var(--boxel-caption-font-size);
1886
+ line-height: var(--boxel-caption-line-height);
1887
+ color: var(--muted-foreground);
1888
+ font-variant-numeric: tabular-nums;
1889
+ }
1890
+ .pretui-toolrow-progress-track {
1891
+ display: block;
1892
+ height: 0.1875rem;
1893
+ border-radius: var(--boxel-border-radius-2xs);
1894
+ overflow: hidden;
1895
+ background-color: var(--inset);
1896
+ }
1897
+ .pretui-toolrow-progress-fill {
1898
+ display: block;
1899
+ height: 100%;
1900
+ background-color: var(--primary);
1901
+ transition: width 200ms
1902
+ var(--pretui-ease, cubic-bezier(0.2, 0, 0.2, 1));
1903
+ }
1904
+ .pretui-toolrow-rows {
1905
+ display: flex;
1906
+ flex-direction: column;
1907
+ gap: var(--boxel-sp-4xs);
1908
+ padding: 0 var(--pretui-toolrow-inset) var(--boxel-sp-xs)
1909
+ var(--pretui-toolrow-text);
1910
+ min-width: 0;
1911
+ }
1912
+ .pretui-toolrow-gate {
1913
+ display: flex;
1914
+ flex-direction: column;
1915
+ gap: var(--boxel-sp-xs);
1916
+ margin: 0 var(--pretui-toolrow-inset) var(--boxel-sp-xs)
1917
+ var(--pretui-toolrow-text);
1918
+ padding: var(--boxel-sp-sm);
1919
+ border-radius: var(--boxel-border-radius-sm);
1920
+ /* the question is a surface, not an alarm: the row's status word
1921
+ and mark carry the attention colour */
1922
+ background-color: var(--inset);
1923
+ box-shadow: inset 0 0 0 1px var(--border);
1924
+ }
1925
+ /* a framed row is already the box (and ringed in the attention
1926
+ colour while it needs the reader): its gate draws no second box,
1927
+ it is the row's next lines. A gate waiting on someone else, in an
1928
+ idle row, gets no attention box either */
1929
+ .pretui-toolrow[data-framed] > .pretui-toolrow-gate {
1930
+ padding: 0;
1931
+ border-radius: 0;
1932
+ background-color: transparent;
1933
+ box-shadow: none;
1934
+ }
1935
+ .pretui-toolrow-result {
1936
+ padding: 0 var(--pretui-toolrow-inset) var(--boxel-sp-xs)
1937
+ var(--pretui-toolrow-text);
1938
+ min-width: 0;
1939
+ }
1940
+ .pretui-toolrow-receipt {
1941
+ display: flex;
1942
+ align-items: center;
1943
+ gap: var(--boxel-sp-xs);
1944
+ min-height: 1.875rem;
1945
+ padding: 0 var(--pretui-toolrow-inset) 0 var(--pretui-toolrow-text);
1946
+ box-shadow: 0 -1px 0 var(--border);
1947
+ font-size: var(--boxel-caption-font-size);
1948
+ line-height: var(--boxel-caption-line-height);
1949
+ font-variant-numeric: tabular-nums;
1950
+ color: var(--subtle-foreground);
1951
+ }
1952
+ .pretui-toolrow-receipt-text {
1953
+ flex: 1;
1954
+ min-width: 0;
1955
+ white-space: nowrap;
1956
+ overflow: hidden;
1957
+ text-overflow: ellipsis;
1958
+ }
1959
+ .pretui-toolrow-receipt-action {
1960
+ flex: none;
1961
+ font-family: var(--font-sans);
1962
+ }
1963
+ /* three widths, one grid. Narrow (a 320px panel): the subtitle and
1964
+ the duration step aside so the title and its state keep their
1965
+ room; the verb stays, it says what kind of call this is. Wide
1966
+ (680px): the subtitle may run longer. The grid itself never
1967
+ changes, so nothing under the header moves. */
1968
+ @container pretui-toolrow (max-width: 359px) {
1969
+ .pretui-toolrow-subtitle,
1970
+ .pretui-toolrow-status:not([data-timed]) .pretui-toolrow-duration {
1971
+ display: none;
1972
+ }
1973
+ /* narrow, the grid gives its width back to the words: the small
1974
+ inset framed or not, and a smaller gap. Every part reads these,
1975
+ so they move together */
1976
+ .pretui-toolrow > * {
1977
+ --pretui-toolrow-inset: var(--boxel-sp-xs);
1978
+ --pretui-toolrow-gap: var(--boxel-sp-2xs);
1979
+ --pretui-toolrow-text: calc(
1980
+ var(--pretui-toolrow-inset) + var(--pretui-toolrow-disc) +
1981
+ var(--pretui-toolrow-gap)
1982
+ );
1983
+ }
1984
+ /* a completed call's title in the settled ink already reads as
1985
+ finished, so its word steps aside. The other done states (kept,
1986
+ delivered, partly kept) keep theirs, and a time always stays */
1987
+ .pretui-toolrow-status[data-state='completed']:not([data-timed]) {
1988
+ display: none;
1989
+ }
1990
+ }
1991
+ @container pretui-toolrow (min-width: 600px) {
1992
+ .pretui-toolrow-subtitle {
1993
+ flex-shrink: 0.4;
1994
+ }
1995
+ }
1996
+ /* Glimmer passes a named block or not, never "sometimes", so a host
1997
+ whose gate or rows are conditional passes the block and lets its
1998
+ content come and go. A section that renders no element takes no
1999
+ space: no empty box, no stray gap. */
2000
+ .pretui-toolrow-actions:not(:has(:not([hidden]))),
2001
+ .pretui-toolrow-receipt:has(.pretui-toolrow-receipt-text:empty):not(
2002
+ :has(.pretui-toolrow-receipt-action *)
2003
+ ),
2004
+ .pretui-toolrow-rows:not(:has(*)),
2005
+ .pretui-toolrow-gate:not(:has(*)),
2006
+ .pretui-toolrow-result:not(:has(*)) {
2007
+ display: none;
2008
+ }
2009
+ /* framed: the same grid with a wider inset, in a box whose ring
2010
+ carries the state. A framed row closes with its own padding when
2011
+ it has no receipt to close it. */
2012
+ .pretui-toolrow[data-framed] {
2013
+ --pretui-toolrow-inset: var(--boxel-sp-sm);
2014
+ border-radius: var(--boxel-border-radius-sm);
2015
+ background-color: var(--card);
2016
+ box-shadow: 0 0 0 1px var(--border);
2017
+ overflow: hidden;
2018
+ }
2019
+ /* a sliver above the header and below the last section, added to
2020
+ each section's own bottom spacing. A receipt is the box's floor,
2021
+ so it takes the sliver back and sits flush */
2022
+ .pretui-toolrow[data-framed] {
2023
+ --_toolrow-edge: var(--boxel-sp-6xs);
2024
+ padding-block: var(--_toolrow-edge);
2025
+ }
2026
+ .pretui-toolrow[data-framed] > .pretui-toolrow-receipt {
2027
+ margin-block-end: calc(-1 * var(--_toolrow-edge));
2028
+ }
2029
+ /* a receipt with neither words nor an action takes no line */
2030
+ .pretui-toolrow-receipt:not(
2031
+ :has(.pretui-toolrow-receipt-text:not(:empty))
2032
+ ):not(:has(.pretui-toolrow-receipt-action *)) {
2033
+ display: none;
2034
+ }
2035
+ /* a framed row with nothing under its header (a settled call whose
2036
+ detail is closed) is one line: it reads as a plain row, so a
2037
+ column of finished calls does not become a column of boxes. The
2038
+ frame returns as soon as anything shows under the header */
2039
+ .pretui-toolrow[data-framed]:not([data-open='true']):not(
2040
+ [data-tone='attention'],
2041
+ [data-tone='failed'],
2042
+ [data-tone='running']
2043
+ ):not(
2044
+ :has(
2045
+ > .pretui-toolrow-progress,
2046
+ > .pretui-toolrow-rows > *,
2047
+ > .pretui-toolrow-gate > *,
2048
+ > .pretui-toolrow-result > *,
2049
+ > .pretui-toolrow-receipt .pretui-toolrow-receipt-text:not(:empty)
2050
+ )
2051
+ ) {
2052
+ --pretui-toolrow-inset: var(--boxel-sp-xs);
2053
+ background-color: transparent;
2054
+ box-shadow: none;
2055
+ padding-block: 0;
2056
+ }
2057
+ /* a framed row's ring stays the border in every state: a state
2058
+ colours the mark and the word, never the row */
2059
+ .pretui-toolrow[data-framed] button.pretui-toolrow-header:hover {
2060
+ background-color: var(--hover);
2061
+ }
2062
+ /* the whole failure is pasteable text: mono, in ink, on the inset
2063
+ well, not a red box */
2064
+ .pretui-toolrow-error {
2065
+ margin: 0;
2066
+ padding: var(--boxel-sp-xs) var(--boxel-sp-sm);
2067
+ border-radius: var(--boxel-border-radius-xs);
2068
+ font-family: var(--font-mono);
2069
+ font-size: var(--boxel-font-size-xs);
2070
+ line-height: var(--boxel-line-height-xs);
2071
+ white-space: pre-wrap;
2072
+ overflow-wrap: anywhere;
2073
+ color: var(--foreground);
2074
+ background-color: var(--inset);
2075
+ }
2076
+ .pretui-toolrow-live {
2077
+ position: absolute;
2078
+ width: 1px;
2079
+ height: 1px;
2080
+ overflow: hidden;
2081
+ clip-path: inset(50%);
2082
+ white-space: nowrap;
2083
+ }
2084
+ }
2085
+ </style>
2086
+ </template>
2087
+ }