@cardstack/boxel-cli 0.11.0-unstable.1 → 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.
- package/bundled-realms/pretui/components/agent-mark.gts +98 -0
- package/bundled-realms/pretui/components/agent-question.gts +340 -37
- package/bundled-realms/pretui/components/answer-footer.gts +249 -0
- package/bundled-realms/pretui/components/card-embed.gts +523 -0
- package/bundled-realms/pretui/components/card-pills.gts +725 -0
- package/bundled-realms/pretui/components/code-block.gts +87 -45
- package/bundled-realms/pretui/components/collapsed-turn.gts +133 -12
- package/bundled-realms/pretui/components/compaction-chip.gts +81 -67
- package/bundled-realms/pretui/components/composer-cap.gts +516 -0
- package/bundled-realms/pretui/components/context-ring.gts +108 -0
- package/bundled-realms/pretui/components/diff-block.gts +1002 -31
- package/bundled-realms/pretui/components/diff-stat.gts +102 -0
- package/bundled-realms/pretui/components/jump-chip.gts +86 -53
- package/bundled-realms/pretui/components/markdown-stream.gts +799 -0
- package/bundled-realms/pretui/components/markdown-surface.gts +71 -0
- package/bundled-realms/pretui/components/plan-mark.gts +147 -0
- package/bundled-realms/pretui/components/prose.gts +185 -8
- package/bundled-realms/pretui/components/prose.md +6 -8
- package/bundled-realms/pretui/components/quick-look.gts +377 -0
- package/bundled-realms/pretui/components/reactions.gts +136 -0
- package/bundled-realms/pretui/components/recommendation.gts +67 -36
- package/bundled-realms/pretui/components/result-card.gts +58 -22
- package/bundled-realms/pretui/components/segmented-control.gts +13 -8
- package/bundled-realms/pretui/components/session-prep.gts +12 -11
- package/bundled-realms/pretui/components/sliding-highlight.gts +8 -3
- package/bundled-realms/pretui/components/speaker-label.gts +132 -0
- package/bundled-realms/pretui/components/state-glyph.gts +121 -0
- package/bundled-realms/pretui/components/step-list.gts +62 -60
- package/bundled-realms/pretui/components/stick-to-bottom.gts +1150 -0
- package/bundled-realms/pretui/components/system-line.gts +143 -0
- package/bundled-realms/pretui/components/task-checklist.gts +184 -0
- package/bundled-realms/pretui/components/thinking.gts +63 -94
- package/bundled-realms/pretui/components/tool-change-table.gts +285 -0
- package/bundled-realms/pretui/components/tool-gate.gts +462 -0
- package/bundled-realms/pretui/components/tool-group.gts +920 -0
- package/bundled-realms/pretui/components/tool-icon.ts +64 -0
- package/bundled-realms/pretui/components/tool-meta.gts +82 -0
- package/bundled-realms/pretui/components/tool-plan.gts +197 -0
- package/bundled-realms/pretui/components/tool-row.gts +2087 -0
- package/bundled-realms/pretui/components/turn-changes.gts +227 -0
- package/bundled-realms/pretui/components/user-bubble.gts +575 -0
- package/bundled-realms/pretui/components/work-item.gts +185 -48
- package/bundled-realms/pretui/docs/scroll-anchoring-protocol.md +136 -0
- package/bundled-realms/pretui/internal/markdown-projection.ts +438 -0
- package/bundled-realms/pretui/internal/mount-queue.ts +79 -0
- package/bundled-realms/pretui/internal/tool-phrases.ts +168 -0
- package/bundled-realms/pretui/package.json +2 -1
- package/bundled-test-harness/assets/{ai-module-creation-test-beRNwH-e.js → ai-module-creation-test-B6FQKqIl.js} +1 -1
- package/bundled-test-harness/assets/{app-QAuw0l8v.js → app-KpPqdyX3.js} +4 -4
- package/bundled-test-harness/assets/{binary-upload-dedupe-test-bI8T0QLl.js → binary-upload-dedupe-test-DN00y1tM.js} +1 -1
- package/bundled-test-harness/assets/{can-invoke-test-DVaU_yRT.js → can-invoke-test-Bl-EUFM3.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-paging-test-DnxMnXKR.js → card-chooser-paging-test-BNuZRHX3.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-test-DUSnLr-u.js → card-chooser-test-wKfPOOT4.js} +1 -1
- package/bundled-test-harness/assets/{card-def-field-def-relationships-test-GBUr3tFK.js → card-def-field-def-relationships-test-CduQ6zan.js} +1 -1
- package/bundled-test-harness/assets/{card-delete-test-CQveO1VC.js → card-delete-test-DKxolj5o.js} +1 -1
- package/bundled-test-harness/assets/{check-correctness-test-D2M2p66N.js → check-correctness-test-B-kgHztp.js} +1 -1
- package/bundled-test-harness/assets/choreo-BEHW-NOc.js +2 -0
- package/bundled-test-harness/assets/{choreo-CJfBb_EY.js → choreo-C2cpCfhA.js} +164 -13
- package/bundled-test-harness/assets/{choreo-feature-reel-test-D2OrygZv.js → choreo-feature-reel-test-CPI_6nkT.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-w07xqtO-.js → choreo-gallery-BxA9cyzF.js} +3 -3
- package/bundled-test-harness/assets/{choreo-gallery-build-order-test-yQg4JER6.js → choreo-gallery-build-order-test-CLVoseAJ.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-compositor-test-C2THCXJN.js → choreo-gallery-compositor-test-BpT5Ia6B.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-cover-test-D4T_Tk4s.js → choreo-gallery-cover-test-DL_fdSsh.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-crossing-stage-test-tHUPJ-Hy.js → choreo-gallery-crossing-stage-test-BcKHhpOK.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-demos-alive-test-BOtBOnEZ.js → choreo-gallery-demos-alive-test-KufLhXG8.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-drift-test-D_6U1Z2c.js → choreo-gallery-drift-test-Cx58rYIA.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-escort-test-DDBU0n4C.js → choreo-gallery-escort-test-Wz-emmID.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-films-test-CgW1NSdr.js → choreo-gallery-films-test-BFrVs5WW.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-filter-test-DUIsNwdO.js → choreo-gallery-filter-test-DCKQkhwe.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-fold-test-Chzlxvdr.js → choreo-gallery-fold-test-UtpDgpG9.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-guide-pages-test-DOWnqALT.js → choreo-gallery-guide-pages-test-rXDU9JLM.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-guides-test-Dra2J4LJ.js → choreo-gallery-guides-test-DfVFdIuU.js} +3 -3
- package/bundled-test-harness/assets/{choreo-gallery-hang-test-zzn1eiB-.js → choreo-gallery-hang-test-Cj7MbkFf.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-highlight-test-CW99hncQ.js → choreo-gallery-highlight-test-D6CF3IYZ.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-inline-edit-test-CYFm1kUz.js → choreo-gallery-inline-edit-test-LfHWsbqc.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-interruption-test-CYIBrKV9.js → choreo-gallery-interruption-test-CvQQAgTp.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-lone-stack-test-DL8R3SG7.js → choreo-gallery-lone-stack-test-D7Zci7fm.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-navigation-test-Dcv04kDx.js → choreo-gallery-navigation-test-DB1EnBEW.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-playhead-transport-test-Cfz7GsXl.js → choreo-gallery-playhead-transport-test-CtBlc6AJ.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-poplayout-subtree-test-CwoklBkW.js → choreo-gallery-poplayout-subtree-test-BQbmjsvI.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-presence-modes-test-BDCIZzJ0.js → choreo-gallery-presence-modes-test-CIdXVG1K.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-presentation-test-DlyiHwFf.js → choreo-gallery-presentation-test-C_rPFp29.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-reduced-motion-test-DVz5SKNR.js → choreo-gallery-reduced-motion-test-CH5w8Lw-.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-sheet-standalone-test-CpD_-BvS.js → choreo-gallery-sheet-standalone-test-CDgI4mGX.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-sheet-test-BjvAKHDL.js → choreo-gallery-sheet-test-5lsFX7Rk.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-site-test-oq_7y_PN.js → choreo-gallery-site-test-DbMa2xeC.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-stage-Bzr4_8Xd.js → choreo-gallery-stage-b3Sk86hp.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-stage-keyboard-test-a83d2euJ.js → choreo-gallery-stage-keyboard-test-ds6fT5Eg.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-stages-test-C9Sl1kH6.js → choreo-gallery-stages-test-ahioKCct.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-subdivision-test-BVVnkOwk.js → choreo-gallery-subdivision-test-g7anTDhT.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-table-plan-test-B0YdYEA0.js → choreo-gallery-table-plan-test-Bozc4uUf.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-theater-test-BLA147xw.js → choreo-gallery-theater-test-kE535d-b.js} +3 -3
- package/bundled-test-harness/assets/{choreo-gallery-tuning-test-DSXiNzCh.js → choreo-gallery-tuning-test-6rHkQBnZ.js} +1 -1
- package/bundled-test-harness/assets/{choreo-gallery-widget-room-test-BI574PdO.js → choreo-gallery-widget-room-test-0Vq-uTo1.js} +2 -2
- package/bundled-test-harness/assets/{choreo-gallery-wires-test-B6Mz2WZl.js → choreo-gallery-wires-test-CdtnAWYO.js} +1 -1
- package/bundled-test-harness/assets/clip-BLfuHi8g.js +2 -0
- package/bundled-test-harness/assets/{clip-C_pXjVvY.js → clip-q1QE5f73.js} +1 -1
- package/bundled-test-harness/assets/{codeblocks-test-AdolhdCA.js → codeblocks-test-BDD7qK_V.js} +1 -1
- package/bundled-test-harness/assets/{compaction-test-DvyAQq56.js → compaction-test-BweolkhG.js} +1 -1
- package/bundled-test-harness/assets/{container-DIF_9lZ9.css → container-B5B6u-xm.css} +29 -22
- package/bundled-test-harness/assets/{container-DvvqmNpg.js → container-BnJjIM6J.js} +9 -6
- package/bundled-test-harness/assets/{create-listing-modal-test-D95JfQab.js → create-listing-modal-test-FvcAKpIa.js} +1 -1
- package/bundled-test-harness/assets/{debug-message-test-BcNtj7Wr.js → debug-message-test-CJ18Evt7.js} +1 -1
- package/bundled-test-harness/assets/{externals-6ZWftYIb.js → externals-BWjhmCD4.js} +12 -12
- package/bundled-test-harness/assets/{fallback-banner-live-delete-test-5lnN1pv2.js → fallback-banner-live-delete-test-C2H3BrT-.js} +1 -1
- package/bundled-test-harness/assets/{fallback-banner-test-DFb_m_Kq.js → fallback-banner-test-C10aYjkj.js} +1 -1
- package/bundled-test-harness/assets/{fallback-models-test-p3YIwj1L.js → fallback-models-test-BxoPiaGF.js} +1 -1
- package/bundled-test-harness/assets/{file-attachment-test-CmyjNWRR.js → file-attachment-test-DeKyzYaz.js} +1 -1
- package/bundled-test-harness/assets/film-B68PjRlr.js +2 -0
- package/bundled-test-harness/assets/{film-D7vtJO5q.js → film-CeUpnHN_.js} +6 -6
- package/bundled-test-harness/assets/{film-DN0A8JHx.js → film-DBZqyqW4.js} +6 -6
- package/bundled-test-harness/assets/{general-test-CX-TFDuX.js → general-test-BewzrpoB.js} +1 -1
- package/bundled-test-harness/assets/{glimmer-motion-and-choreo-in-cards-test-BHte2t9G.js → glimmer-motion-and-choreo-in-cards-test-HA-INN3d.js} +14 -14
- package/bundled-test-harness/assets/{glimmer-motion-and-choreo-test-BPNVeIio.js → glimmer-motion-and-choreo-test-BZ0ZuuAw.js} +3 -3
- package/bundled-test-harness/assets/{index-query-engine-test-Dz7Q4Xmk.js → index-query-engine-test-BtI8Y2LR.js} +1 -1
- package/bundled-test-harness/assets/{instance-filter-matcher-test-BoLU3bVg.js → instance-filter-matcher-test-BHXHGHjE.js} +1 -1
- package/bundled-test-harness/assets/{main-Dlgp8c7v.js → main-CqAHZ2kK.js} +1 -1
- package/bundled-test-harness/assets/{message-tool-test-CfjRSJvt.js → message-tool-test-NMaPJFsd.js} +1 -1
- package/bundled-test-harness/assets/{model-cost-tier-test-mZG84Uso.js → model-cost-tier-test-BgbfN2LH.js} +1 -1
- package/bundled-test-harness/assets/{operation-lowering-test-B0KpI1xu.js → operation-lowering-test-C4IaOjsA.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-card-chooser-test-KWS8lq16.js → operator-mode-card-chooser-test-Bwe47Rn7.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-links-test-CViFt0Ws.js → operator-mode-links-test-BU4jkC-B.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-test-D0gZM2yw.js → operator-mode-test-Bh0rGM9z.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-ui-test-CEvHJ9DS.js → operator-mode-ui-test-whgUw0ju.js} +1 -1
- package/bundled-test-harness/assets/{overlay-menu-items-test-Pw80RXsr.js → overlay-menu-items-test-Cwn1vr8q.js} +1 -1
- package/bundled-test-harness/assets/{overlays-BnOM360u.js → overlays-BOzLbVDK.js} +1 -1
- package/bundled-test-harness/assets/{overlays-kBVfa4i8.js → overlays-C_PuGDGh.js} +1 -1
- package/bundled-test-harness/assets/{past-sessions-test-CW0-Bjav.js → past-sessions-test-Bh24N-Kz.js} +1 -1
- package/bundled-test-harness/assets/{permission-checking-realm-test-D5uXQypS.js → permission-checking-realm-test-DKnnAy0f.js} +1 -1
- package/bundled-test-harness/assets/{plate-CFvCDv2t.js → plate-BL7UtyKE.js} +1 -1
- package/bundled-test-harness/assets/plate-CqG3igCJ.js +2 -0
- package/bundled-test-harness/assets/player-BJfsgdSI.js +2 -0
- package/bundled-test-harness/assets/{player-DzuYjrTU.js → player-Da_CIsp_.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-policy-standing-test-Btatheni.js → realm-config-policy-standing-test-DngdYypq.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-routing-rule-edit-test-Dh5zW13o.js → realm-config-routing-rule-edit-test-_rkFU8SN.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-settings-test-BJa8gv_C.js → realm-config-settings-test-CR9NH0Zn.js} +1 -1
- package/bundled-test-harness/assets/{reasoning-test-2Zp37cQB.js → reasoning-test-0lH10kRl.js} +1 -1
- package/bundled-test-harness/assets/{room-message-test-BsrON13e.js → room-message-test-C-PddE4Y.js} +1 -1
- package/bundled-test-harness/assets/{run-BMyvn_UL.js → run-D4n4LPcK.js} +96 -6
- package/bundled-test-harness/assets/{run-realm-code-BxktQf6R.js → run-realm-code-C8N7DtN-.js} +1 -1
- package/bundled-test-harness/assets/{run-realm-code-test-7grpTos-.js → run-realm-code-test-DBdp147q.js} +1 -1
- package/bundled-test-harness/assets/{schema-generation-test-CM9ggQji.js → schema-generation-test-CB1Hb7GQ.js} +1 -1
- package/bundled-test-harness/assets/{scrolling-test-BER00htB.js → scrolling-test-3PhVin8h.js} +1 -1
- package/bundled-test-harness/assets/{searchable-option-test-BBr5BPow.js → searchable-option-test-BAdOiL4U.js} +1 -1
- package/bundled-test-harness/assets/{sending-test-BJGobs2E.js → sending-test-KSmu1tSJ.js} +1 -1
- package/bundled-test-harness/assets/{skills-test-GPOsHRN6.js → skills-test-D-dlcFpd.js} +1 -1
- package/bundled-test-harness/assets/{src-Df148w_b.js → src-B_DEhPze.js} +4 -4
- package/bundled-test-harness/assets/{src-Cf2wv-Le.js → src-BoTgwqUA.js} +2 -2
- package/bundled-test-harness/assets/{store-test-QvvHimzH.js → store-test-CEW7xQjZ.js} +1 -1
- package/bundled-test-harness/assets/{test-support-rNHuieEc.js → test-support-FXDPpIl3.js} +1 -1
- package/bundled-test-harness/assets/{test-support-C6N_2X-l.js → test-support-VX6ukQNR.js} +1 -1
- package/bundled-test-harness/assets/{tests-CmsQ-Z_3.js → tests-QYqqa6nv.js} +84 -84
- package/bundled-test-harness/assets/{text-input-validator-test-CYYH2N0L.js → text-input-validator-test-D8iNOWep.js} +1 -1
- package/bundled-test-harness/assets/{titles-B1pTaIfP.js → titles-DFJj5O0E.js} +1 -1
- package/bundled-test-harness/assets/titles-xGlgu7ys.js +2 -0
- package/bundled-test-harness/assets/{to-device-streaming-test-Ctqa0bVp.js → to-device-streaming-test-trCX-505.js} +1 -1
- package/bundled-test-harness/assets/{token-usage-test-BQVbK3X0.js → token-usage-test-C6sULooH.js} +1 -1
- package/bundled-test-harness/assets/{tools-DU8Rc15s.js → tools-UJfD-_al.js} +1 -1
- package/bundled-test-harness/assets/{tools-test-B13yvEKE.js → tools-test-Brav31AV.js} +1 -1
- package/bundled-test-harness/assets/{worker-DHBEBihX.js → worker-9ph9_Lpv.js} +31 -26
- package/bundled-test-harness/index.html +8 -8
- package/bundled-test-harness/tests/index.html +8 -8
- package/bundled-types/choreo/beacons.ts +16 -4
- package/bundled-types/choreo/changeset.ts +25 -5
- package/bundled-types/choreo/choreo.gts +245 -3
- package/bundled-types/choreo/far.ts +39 -0
- package/bundled-types/choreo/index.ts +2 -0
- package/bundled-types/choreo/run.ts +104 -1
- package/bundled-types/host-app/lib/realm-runner/worker.ts +8 -51
- package/bundled-types/runtime-common/realm-runner-script.d.ts +3 -0
- package/package.json +1 -1
- package/bundled-test-harness/assets/choreo-B3Z9QhDH.js +0 -2
- package/bundled-test-harness/assets/clip-BjcXuTwN.js +0 -2
- package/bundled-test-harness/assets/film-DMDPP7ma.js +0 -2
- package/bundled-test-harness/assets/plate-66qnQ-M9.js +0 -2
- package/bundled-test-harness/assets/player-pygg1PwA.js +0 -2
- package/bundled-test-harness/assets/titles-Ccpcl-jq.js +0 -2
|
@@ -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
|
+
}
|