@jini-ai/chat 0.6.4 → 0.6.5
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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.6.5 — 2026-10-15
|
|
2
|
+
|
|
3
|
+
- Durable recovery no longer gives up on a finished CLI turn (8c6acfea): a saved successful Claude CLI `result` line carries `modelUsage.contextWindow`, which matched the context-limit pattern case-insensitively, so a dropped stream or restart ended the answer with "The assistant could not continue this answer" instead of continuing it. The pattern now requires a separator, and attempt diagnostics skip successful CLI result lines (`type: "result"`, `is_error: false`).
|
|
4
|
+
- `recover()` logs one `[durable-recovery]` info line per decision (9cc967cd) with runId, trigger, probe, decision and failure class only; `logger` is an optional injectable, default console.
|
|
5
|
+
- On phones and touch screens the attachment preview's Close and previous/next get 44px tap areas (3b6a5480); desktop is unchanged.
|
|
6
|
+
|
|
1
7
|
## 0.6.4 — 2026-10-15
|
|
2
8
|
|
|
3
9
|
- Image preview steps through a message's or the composer's images (2444ac4d): clicking a sent image or a staged chip opens the preview; with more than one image, previous/next buttons (bottom-right) and the arrow keys step through them; Esc or a backdrop click closes it and focus returns to the thumbnail. Staged chips show a thumbnail, labelled "Image" when the name is only a content hash.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CHAT_PANE_STYLES = "\n.jini-chat-pane {\n --jini-chat-bg: #faf9f7;\n --jini-chat-panel: #fdfcfa;\n --jini-chat-text: #1a1916;\n --jini-chat-text-strong: #0d0c0a;\n --jini-chat-muted: #74716b;\n --jini-chat-faint: #b3b0a8;\n --jini-chat-border: #e1e5eb;\n --jini-chat-border-strong: #c9d0da;\n --jini-chat-border-soft: #edf0f4;\n --jini-chat-subtle: #f4f5f7;\n --jini-chat-accent: #c96442;\n --jini-chat-accent-soft: #fbeee5;\n --jini-chat-danger: #e5484d;\n --jini-chat-radius: 8px;\n --jini-chat-radius-lg: 12px;\n --bg-panel: var(--jini-chat-panel);\n --bg-subtle: var(--jini-chat-subtle);\n --border: var(--jini-chat-border);\n --border-strong: var(--jini-chat-border-strong);\n --text: var(--jini-chat-text);\n --text-strong: var(--jini-chat-text-strong);\n --text-muted: var(--jini-chat-muted);\n --danger: var(--jini-chat-danger);\n --radius: var(--jini-chat-radius);\n --radius-lg: var(--jini-chat-radius-lg);\n position: relative;\n display: grid;\n grid-template-rows: auto minmax(0, 1fr) auto;\n min-width: 0;\n min-height: 0;\n height: 100%;\n /* clip after the hidden fallback: a hidden box still scrolls programmatically (a focus() or\n scrollIntoView() inside it), which shifts the pinned composer up and leaves blank pane below. */\n overflow: hidden;\n overflow: clip;\n color: var(--jini-chat-text);\n background: var(--jini-chat-bg);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Microsoft YaHei UI\", \"Noto Sans\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n font-size: 13.5px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.jini-chat-pane button,\n.jini-runtime-popover button,\n.jini-runtime-popover select {\n font: inherit;\n}\n.jini-chat-pane__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n min-height: 74px;\n padding: 18px 22px 14px;\n border-bottom: 1px solid var(--jini-chat-border-soft);\n}\n.jini-chat-pane__heading { min-width: 0; }\n.jini-chat-pane__eyebrow {\n display: block;\n margin-bottom: 4px;\n color: var(--jini-chat-muted);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 10px;\n letter-spacing: .1em;\n text-transform: uppercase;\n}\n.jini-chat-pane__title {\n overflow: hidden;\n margin: 0;\n font-size: 21px;\n font-weight: 650;\n letter-spacing: -.025em;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane__new-thread {\n padding: 8px 12px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-chat-pane__body {\n position: relative;\n min-height: 0;\n overflow: hidden;\n overflow: clip;\n}\n.jini-chat-pane .jini-message-list {\n height: 100%;\n overflow-y: auto;\n /*\n * .jini-chat-pane__controls is an absolutely-positioned overlay (not a flex/grid sibling that\n * pushes this list up), so its real height has to be reserved here instead. This used to be a\n * flat 240px guess -- and had already gone stale once before that: it started at 170px, a live\n * render outgrew it (composer + footer + working-directory row alone measured 179.75px, clipping\n * the last message's tail under the overlay), and 240px was chosen as a margin above THAT\n * measurement, not a value anything keeps in sync. useChatPaneControlsHeight now measures the\n * overlay's real height live via ResizeObserver and publishes it as\n * --jini-chat-controls-height on the pane root, so this reservation tracks the composer's\n * actual content (an attachment tray, a wrapped multi-line draft, a taller runtime-picker\n * popover) instead of a number that is only ever correct until the next thing that grows it.\n * 240px survives as the fallback for the rare environment without ResizeObserver (SSR; a test\n * harness that never polyfills it) -- everywhere else, the calc() wins.\n */\n padding: 20px 22px calc(var(--jini-chat-controls-height, 240px) + 24px);\n scrollbar-width: thin;\n}\n.jini-chat-pane .jini-message {\n margin-bottom: 20px;\n color: var(--jini-chat-text);\n font-size: 14px;\n line-height: 1.62;\n}\n/*\n * Off-screen rows skip style, layout and paint. Owner report 2026-10-11: typing in a long chat\n * lagged. React was not the cost (no row re-renders per key); the browser was. A keystroke's DOM\n * change can invalidate style for the whole document (a host dock's :has() rule did),\n * and a transcript with ~2,000 tool cards and 3.5 MB of tool output then cost ~50 ms of style\n * recalc per key. With this, only the rows on screen are recalculated (~8 ms). The DOM stays\n * whole: find-in-page, textContent readers and screen readers still see every row.\n * 'auto' in contain-intrinsic-size keeps a row's last real height once it has rendered; the\n * auto-scroll ResizeObserver re-sticks to the bottom as estimated rows resolve.\n * Containment clips paint at the row's edge, so overflow-clip-margin leaves room for focus rings\n * (2px outline + 2px offset); @supports keeps browsers without it on the old, unclipped path.\n */\n@supports (overflow-clip-margin: 8px) {\n .jini-chat-pane .jini-message {\n content-visibility: auto;\n contain-intrinsic-size: auto 160px;\n overflow-clip-margin: 8px;\n }\n}\n/* A mid-run user bubble is nested inside an assistant turn, so the preceding tool/text block\n * has no message-row bottom margin. Give it the same separation as an ordinary next turn. */\n.jini-chat-pane .jini-message-user--mid-run {\n margin-top: 20px;\n}\n/*\n * Bubble on the content, not the row: MessageRow.tsx renders attachments as siblings of\n * .jini-message-content inside .jini-message-user, each with their own chip styling -- bubbling\n * the whole row would nest one background box inside another. This replaces a dead rule this file\n * shipped with (.jini-message-row[data-role=\"user\"] / .jini-message-row.user): MessageRow.tsx\n * has never rendered a jini-message-row class or a data-role attribute, and a user message's\n * root class is jini-message-user, not bare \"user\" -- the selector never matched anything real.\n */\n.jini-chat-pane .jini-message-user .jini-message-content {\n margin-left: auto;\n padding: 10px 13px;\n width: fit-content;\n max-width: 88%;\n /* User bubbles share this style at every nesting depth, regardless of message length. */\n background: var(--jini-chat-user-bubble, #1a1b1c);\n color: var(--jini-chat-user-bubble-ink, #fff);\n border-radius: 12px 12px 3px 12px;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachments {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n justify-content: flex-end;\n margin-bottom: 4px;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip {\n font: inherit;\n font-size: 11px;\n line-height: 1.4;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n padding: 3px 10px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip:hover {\n color: var(--jini-chat-text);\n border-color: var(--jini-chat-border-strong);\n}\n/* A sent image this browser still holds (SentAttachment.tsx): a small square thumbnail instead of\n * the filename pill. Same click target and preview modal; cover-cropped so mixed aspect ratios line up. */\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip.is-thumbnail {\n width: 64px;\n height: 64px;\n padding: 0;\n overflow: hidden;\n border-radius: 10px;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip.is-thumbnail:focus-visible {\n outline: 2px solid var(--jini-chat-accent);\n outline-offset: 2px;\n}\n.jini-chat-pane .jini-message-attachment-thumb {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n.jini-chat-pane .jini-message-agent {\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .02em;\n text-transform: uppercase;\n color: var(--jini-chat-faint);\n margin-bottom: 4px;\n}\n.jini-chat-pane .jini-message-error { color: var(--danger); font-size: 13px; }\n.jini-chat-pane .jini-message-pending { color: var(--jini-chat-muted); font-size: 13px; font-style: italic; }\n/*\n * The pending-turn strip: a composer-driven turn typed while a run is already streaming, rendered\n * as the newest entry in the transcript (MessageList.tsx's pendingPrompt prop) rather than a\n * banner bolted above the composer \u2014 matching Claude Code/ChatGPT's own pending-message UX\n * (previously ChatPane.tsx's ChatPaneQueuedPrompt, which rendered between the drop-target's drag\n * announcement and the composer; that component is gone).\n *\n * Owner's own framing was \"why isn't the queue message just put in the chat\" \u2014 so\n * .jini-chat-pane__queued-text below is deliberately NOT a variant banner shape; it copies\n * .jini-message-user .jini-message-content's bubble geometry line for line (width: fit-content,\n * max-width: 88%, same padding/border-radius) so it reads as the operator's own message, not a\n * system notice. What marks it as NOT YET SENT is two properties a real sent bubble never carries:\n * a dashed border (a sent bubble has none at all) and reduced opacity \u2014 chosen over the old boxed\n * strip-plus-label treatment because those two are legible at a glance without adding a second\n * visual language next to the transcript's own. The former \"QUEUED\" chip label is gone for the same\n * reason: once the bubble itself reads as pending, a label restates it. role=\"status\" plus the\n * aria-label on the row (MessageList.tsx) still carry that meaning to assistive tech that can't see\n * the dashed border. The cancel action is a separate, smaller line below the bubble (mirroring\n * .jini-message-actions--user's own placement under a real message) rather than inline inside it,\n * so it reads as secondary to the bubble, not competing with it.\n *\n * Reuses this theme's own tokens rather than the currentColor/color-mix approach reference.css uses\n * for the same rules \u2014 that file is deliberately token-free because it ships unstyled and undocked\n * from any root that defines custom properties, but this stylesheet's .jini-chat-pane root already\n * defines concrete --jini-chat-* values for every other rule here, so matching that (not\n * reference.css's host-agnostic fallback) is the actual local convention.\n * NOTE: this comment avoids backtick quoting on purpose, same as the cancel-button comment below \u2014\n * the whole stylesheet lives inside a JS template literal, so a literal backtick here would\n * terminate the string, not just this comment.\n */\n.jini-chat-pane__queued {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 4px;\n margin: 4px 0 0;\n}\n.jini-chat-pane__queued-text {\n width: fit-content;\n max-width: 88%;\n padding: 10px 13px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-subtle);\n border: 1px dashed var(--jini-chat-border);\n border-radius: 12px 12px 3px 12px;\n opacity: .68;\n overflow-wrap: break-word;\n}\n.jini-chat-pane__queued-actions {\n display: flex;\n justify-content: flex-end;\n}\n/*\n * A plain-text cancel affordance below the pending bubble, not a primary action \u2014 without this\n * reset a host's own global 'button' styling (background, border, padding, border-radius, bold\n * text) paints it as a full pill-shaped button that visually competes with (and can overlap) the\n * transcript around it \u2014 the exact failure an operator hit live in one host. '.jini-chat-pane\n * button { font: inherit }' near the top of this file already resets font-family/line-height/etc\n * to this pane's own ambient values; font-size/font-weight are redeclared here (not just left at\n * that inherited ambient size) to size this specific control down to a caption scale, well under\n * the bubble's own 14px text.\n * NOTE: this comment avoids backtick quoting on purpose (see this file's own precedent further\n * down) \u2014 the whole stylesheet lives inside a JS template literal, so a literal backtick here\n * would terminate the string, not just this comment.\n */\n.jini-chat-pane__queued-cancel {\n padding: 2px 6px;\n color: var(--jini-chat-muted);\n background: none;\n border: 0;\n border-radius: 6px;\n font-size: 11px;\n font-weight: 400;\n cursor: pointer;\n}\n.jini-chat-pane__queued-cancel:hover {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-border);\n}\n/*\n * Per-message action row (MessageRow.tsx's 'CopyMessageButton', Icon.tsx's 'copy' glyph): quiet,\n * icon-only, always present rather than a hover-only reveal, so keyboard and touch users get the\n * same access a mouse hover would give. The assistant variant carries a thin left rail (its own\n * left border) to read as attached to the turn above it without repeating that message's own\n * chrome; the user variant skips the rail and instead mirrors its bubble's own right alignment.\n * Copy is the only live button today \u2014 'jini-message-actions--assistant' is an intentional\n * extension seam, not the finished row; see that row's own call-site comment in MessageRow.tsx.\n */\n.jini-chat-pane .jini-message-actions {\n display: flex;\n align-items: center;\n gap: 2px;\n margin-top: 6px;\n}\n.jini-chat-pane .jini-message-actions--assistant {\n padding-left: 8px;\n border-left: 2px solid var(--jini-chat-border-soft);\n}\n.jini-chat-pane .jini-message-actions--user { justify-content: flex-end; }\n.jini-chat-pane .jini-message-action-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n padding: 0;\n color: var(--jini-chat-faint);\n background: none;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-message-action-btn:hover { color: var(--jini-chat-muted); background: var(--jini-chat-subtle); }\n.jini-chat-pane .jini-message-action-btn:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 1px; }\n/* Visually hidden but present in the DOM (never display:none) so the polite live region beside\n each copy button still gets announced \u2014 same clip-rect technique this package already uses for\n its other visually-hidden-but-functional controls. */\n.jini-chat-pane .jini-message-copy-status {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n}\n.jini-chat-pane__controls {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 4;\n padding: 0 16px 14px;\n background: linear-gradient(to bottom, transparent, var(--jini-chat-bg) 22px);\n}\n.jini-chat-pane__suggestions {\n display: flex;\n gap: 8px;\n margin: 0 2px 8px;\n overflow-x: auto;\n}\n.jini-chat-pane__suggestion {\n flex: 0 0 auto;\n max-width: 290px;\n overflow: hidden;\n padding: 7px 11px;\n color: var(--jini-chat-muted);\n background: color-mix(in srgb, var(--jini-chat-panel) 92%, transparent);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n font-size: 11px;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n}\n.jini-chat-pane__error {\n margin: 0 2px 8px;\n padding: 8px 10px;\n color: #8c2f20;\n background: #fff1ed;\n border: 1px solid #f2c8bb;\n border-radius: 8px;\n font-size: 12px;\n}\n.jini-chat-pane__error-action {\n padding: 0;\n color: inherit;\n background: none;\n border: 0;\n font: inherit;\n font-weight: 650;\n text-decoration: underline;\n cursor: pointer;\n}\n.jini-chat-pane__error-action:hover { opacity: .82; }\n.jini-chat-pane__status {\n margin: 0 2px 8px;\n padding: 8px 10px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n font-size: 12px;\n}\n.jini-chat-pane__drop-target {\n position: relative;\n margin: -8px -8px -8px;\n padding: 6px;\n border: 2px dashed transparent;\n border-radius: 16px;\n transition:\n background-color .14s ease,\n border-color .14s ease,\n box-shadow .14s ease;\n}\n.jini-chat-pane__drop-target.is-dragging-files {\n background: color-mix(in srgb, var(--jini-chat-accent-soft) 72%, transparent);\n border-color: var(--jini-chat-accent);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--jini-chat-accent) 15%, transparent);\n}\n.jini-chat-pane__drop-target.is-dragging-files .jini-composer {\n background: color-mix(in srgb, var(--jini-chat-accent-soft) 58%, var(--jini-chat-panel));\n border-color: color-mix(in srgb, var(--jini-chat-accent) 68%, var(--jini-chat-border));\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--jini-chat-accent) 10%, transparent);\n}\n.jini-chat-pane__drop-announcement {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n clip-path: inset(50%);\n}\n.jini-chat-pane .jini-composer {\n position: relative;\n overflow: visible;\n padding: 0;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: none;\n transition: border-color .16s ease, box-shadow .16s ease;\n}\n.jini-chat-pane .jini-composer:focus-within {\n background: var(--jini-chat-panel);\n border-color: color-mix(in srgb, var(--jini-chat-accent) 22%, var(--jini-chat-border-strong));\n box-shadow: 0 1px 2px rgba(26, 25, 22, .06), 0 0 0 1px rgba(201, 100, 66, .06);\n}\n/*\n * Pinned-context zone: whatever a host pins above the composer input (selected plugins, MCP\n * servers, or anything a future host adds -- see 'leadingAccessories''s own doc in 'slots.ts').\n * 'Composer.tsx' only mounts this element when 'slots.leadingAccessories' is a truthy prop value,\n * but a host that always supplies a TRAY COMPONENT rather than conditionally supplying the prop\n * itself (one that internally renders 'null' when nothing is pinned) still leaves an empty node\n * behind -- ':empty' below is what actually collapses THAT case to zero height and zero seam,\n * mirroring '.jini-attachment-tray:empty' a few rules down, which solves the identical problem for\n * the real file-attachment tray.\n *\n * Collapsed (nothing pinned): zero height, zero padding, invisible (0-width) seam -- no reserved\n * space, matching the owner's \"if nil, it doesn't animate\" requirement exactly, since there is\n * nothing here to animate.\n *\n * Populated: grows to a single fixed-height row via 'max-height' (not 'height: auto', which can't\n * transition) and gains a hairline seam using the SAME border/token '.jini-composer-footer' below\n * already uses one zone down -- reads as one more zone of the same control, not a new floating\n * box. The transition runs both directions, so removing the last pinned item gets a matching exit,\n * not an instant cut.\n *\n * 'overflow-x: auto' turns this into a single scrollable row instead of the wrap-and-grow a naive\n * flex row would do with six or more items; '> *' keeps any direct host child (a single tray\n * wrapper, a bare row of buttons, anything) from shrinking to fit, so overflow -- not internal\n * wrapping -- is what kicks in once content is wider than the zone.\n *\n * The \"more content\" cue is a real shadow/vignette, not a plain alpha fade -- an alpha fade toward\n * this zone's own background was tried first and rejected on review: the chip and the zone\n * background are already close in luminance, so fading a chip's OWN pixels toward transparent read\n * as \"something is clipped/broken\", not \"swipe for more\", at a glance with no hover. The four-layer\n * 'background' below is the standard scroll-shadow technique instead: a real 'color-mix'-tinted\n * vignette (darkens in a light theme, lightens in a dark one, since it's mixed from the theme's OWN\n * text color rather than a fixed hex) drawn at each edge -- but each 'scroll'-attached shadow layer\n * is covered by a same-edge 'local'-attached solid-to-transparent layer that scrolls WITH the\n * content, so the shadow is only ever visible on the edges that still have more to reveal: at rest\n * (scrolled to the start) the left shadow is hidden and the right one shows; scroll all the way to\n * the true end and the right shadow hides too. Verified live (not just by reading the technique):\n * scrolling this element from 0 to its max scrollLeft measurably fades the right cover's computed\n * background-position across the full track and the shadow disappears exactly at the end.\n */\n.jini-chat-pane .jini-composer-leading {\n display: flex;\n align-items: center;\n overflow-x: auto;\n overflow-y: hidden;\n max-height: 0;\n padding: 0 12px;\n border-bottom: 0 solid var(--jini-chat-border-soft);\n opacity: 0;\n scrollbar-width: thin;\n transition:\n max-height .22s cubic-bezier(.3, .1, .25, 1),\n padding .22s cubic-bezier(.3, .1, .25, 1),\n border-bottom-width .22s ease,\n opacity .16s ease;\n background-color: var(--jini-chat-subtle);\n background-repeat: no-repeat;\n background-attachment: local, local, scroll, scroll;\n background-position: 0 0, 100% 0, 0 0, 100% 0;\n background-size: 26px 100%, 26px 100%, 20px 100%, 20px 100%;\n background-image:\n linear-gradient(to right, var(--jini-chat-subtle), transparent),\n linear-gradient(to left, var(--jini-chat-subtle), transparent),\n linear-gradient(to right, color-mix(in srgb, var(--jini-chat-text) 30%, transparent), transparent),\n linear-gradient(to left, color-mix(in srgb, var(--jini-chat-text) 30%, transparent), transparent);\n}\n.jini-chat-pane .jini-composer-leading:not(:empty) {\n max-height: 64px;\n padding: 10px 12px;\n border-bottom-width: 1px;\n opacity: 1;\n}\n.jini-chat-pane .jini-composer-leading > * {\n flex: none;\n min-width: max-content;\n}\n@media (prefers-reduced-motion: reduce) {\n .jini-chat-pane .jini-composer-leading { transition: none; }\n}\n.jini-chat-pane .jini-attachment-tray {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n max-height: 88px;\n overflow-y: auto;\n padding: 8px 10px 0;\n scrollbar-width: thin;\n}\n.jini-chat-pane .jini-attachment-tray:empty { display: none; }\n/*\n * The sanctioned reuse path (see '.jini-composer-leading''s own doc above and 'AttachmentTray''s\n * module doc): a host nesting Jini's own tray/chip classes inside the pinned-context zone to match\n * the real attachment tray's look. Un-does the wrap-and-grow rule directly above -- meant for that\n * tray's OWN use one zone down, where wrapping onto a second line is fine -- so the OUTER zone's\n * single scroll axis is the one that wins here instead. The real file-attachment tray is\n * unaffected: it always renders as this zone's sibling ('Composer.tsx'), never its descendant.\n */\n.jini-chat-pane .jini-composer-leading .jini-attachment-tray {\n flex-wrap: nowrap;\n max-height: none;\n overflow: visible;\n padding: 0;\n}\n/* Half the tray's width, so a sidebar-width pane fits two chips per row and its 88px cap shows two\n rows (four chips) before scrolling; at full width each chip took a row and the third was hidden. */\n.jini-chat-pane .jini-attachment-chip {\n display: inline-flex;\n align-items: center;\n box-sizing: border-box;\n min-width: 0;\n max-width: min(calc(50% - 3px), 290px);\n height: 36px;\n padding: 3px 4px 3px 5px;\n color: var(--jini-chat-text);\n background: color-mix(in srgb, var(--jini-chat-panel) 88%, var(--jini-chat-subtle));\n border: 1px solid var(--jini-chat-border);\n border-radius: 9px;\n box-shadow: 0 1px 1px rgba(13, 12, 10, .025);\n animation: jini-chat-chip-in .16s ease both;\n}\n.jini-chat-pane .jini-composer-leading .jini-attachment-chip { max-width: min(100%, 290px); }\n@keyframes jini-chat-chip-in {\n from { opacity: 0; transform: translateY(3px) scale(.94); }\n to { opacity: 1; transform: none; }\n}\n@media (prefers-reduced-motion: reduce) {\n .jini-chat-pane .jini-attachment-chip { animation: none; }\n}\n.jini-chat-pane .jini-attachment-chip-body {\n display: flex;\n align-items: center;\n min-width: 0;\n gap: 7px;\n /* A button (opens the preview modal), reset to the chip's own look. */\n padding: 0;\n font: inherit;\n color: inherit;\n text-align: left;\n background: none;\n border: 0;\n border-radius: 7px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-attachment-chip-body:focus-visible {\n outline: 2px solid var(--jini-chat-accent);\n outline-offset: 2px;\n}\n/* A staged image this browser holds: the thumbnail takes the icon tile's place, same size. */\n.jini-chat-pane .jini-attachment-chip-thumb {\n display: block;\n flex: 0 0 auto;\n width: 26px;\n height: 26px;\n object-fit: cover;\n border-radius: 7px;\n background: var(--jini-chat-subtle);\n}\n.jini-chat-pane .jini-attachment-chip-icon {\n display: grid;\n flex: 0 0 auto;\n place-items: center;\n width: 26px;\n height: 26px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border-radius: 7px;\n}\n.jini-chat-pane .jini-attachment-chip-icon.is-image {\n color: var(--jini-chat-accent);\n background: var(--jini-chat-accent-soft);\n}\n.jini-chat-pane .jini-attachment-chip-copy {\n display: flex;\n min-width: 0;\n flex-direction: column;\n line-height: 1.15;\n}\n.jini-chat-pane .jini-attachment-chip-name {\n overflow: hidden;\n font-size: 11.5px;\n font-weight: 550;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-attachment-chip-size {\n color: var(--jini-chat-muted);\n font-size: 9.5px;\n}\n.jini-chat-pane .jini-attachment-remove {\n display: grid;\n flex: 0 0 auto;\n place-items: center;\n width: 25px;\n height: 25px;\n margin-left: 4px;\n padding: 0;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 0;\n border-radius: 7px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-attachment-remove:hover,\n.jini-chat-pane .jini-attachment-remove:focus-visible {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n outline: none;\n}\n/* Phones: the 25px \"x\" gets an invisible 44px tap area; the chip keeps its size. The area ends at the\n chip's bottom edge and grows up into the tray's 8px top padding / row gap, because growing down\n past the last row would make the 88px tray scroll by a few pixels. */\n@media (max-width: 640px), (pointer: coarse) {\n .jini-chat-pane .jini-attachment-remove { position: relative; }\n .jini-chat-pane .jini-attachment-remove::before {\n content: '';\n position: absolute;\n bottom: -5.5px;\n left: 50%;\n width: 44px;\n height: 44px;\n transform: translateX(-50%);\n }\n}\n.jini-chat-pane .jini-composer-input {\n display: block;\n box-sizing: border-box;\n width: 100%;\n min-height: 92px;\n resize: none;\n padding: 12px 14px 8px;\n color: var(--jini-chat-text);\n background: transparent;\n border: 0;\n outline: 0;\n font-size: 13.5px;\n line-height: 1.6;\n}\n/* Shared focus-zoom floor: embeds at every width, all panes on small/touch viewports. */\n.jini-chat-embed .jini-chat-pane .jini-composer-input { font-size: 16px; }\n@media (max-width: 640px), (pointer: coarse) {\n .jini-chat-pane.jini-chat-pane .jini-composer-input { font-size: 16px; }\n}\n.jini-chat-pane .jini-composer-input::placeholder { color: var(--jini-chat-faint); }\n.jini-chat-pane .jini-composer-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n min-height: 42px;\n gap: 6px;\n padding: 6px 8px 8px;\n border-top: 1px solid var(--jini-chat-border-soft);\n}\n.jini-chat-pane .jini-composer-attachment-picker { display: inline-flex; }\n/* Host controls slotted at the start of the footer row, right after the attach/discovery \"+\"\n trigger above -- see ComposerSlots.footerLeadingAccessory's own doc (slots.ts) for why this\n exists as a third footer slot alongside footerAccessories/plusMenuItems. */\n.jini-chat-pane .jini-composer-footer-leading { display: inline-flex; align-items: center; }\n/* NOT 'position: relative' \u2014 the discovery popover (below) anchors off '.jini-composer' itself\n (the nearest positioned ancestor once this wrapper opts out), not off this small trigger-button\n wrapper. Anchoring to the wrapper put the popover's bottom edge at the wrapper's own top edge \u2014\n inside the footer, just below the textarea \u2014 so any popover taller than a couple of rows grew\n upward straight over the textarea and blocked clicks into it (reproduced live: Playwright's own\n click on the textarea failed with \"intercepts pointer events\" while the popover was open). */\n.jini-chat-pane .jini-composer-discovery { display: inline-flex; }\n.jini-chat-pane .jini-composer-discovery-menu,\n.jini-chat-pane .jini-composer-slash-menu {\n position: absolute;\n z-index: 8;\n display: flex;\n flex-direction: column;\n gap: 4px;\n max-height: 280px;\n overflow-y: auto;\n padding: 8px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 10px;\n box-shadow: 0 12px 30px rgb(0 0 0 / 14%);\n}\n/* Both anchor off '.jini-composer' ('bottom: calc(100% + 6px)' = fully above the textarea AND the\n footer, not just above the trigger button) so the popover floats as a clean detached card instead\n of overlapping the input it sits next to. */\n.jini-chat-pane .jini-composer-discovery-menu {\n inset-inline-start: 8px;\n bottom: calc(100% + 6px);\n width: min(280px, calc(100vw - 32px));\n}\n.jini-chat-pane .jini-composer-slash-menu {\n inset-inline: 8px;\n bottom: calc(100% + 6px);\n}\n.jini-chat-pane .jini-composer-discovery-group { display: flex; flex-direction: column; gap: 2px; }\n.jini-chat-pane .jini-composer-discovery-group-label {\n padding: 6px 8px 2px;\n color: var(--jini-chat-faint);\n font-size: 10px;\n font-weight: 700;\n letter-spacing: .08em;\n text-transform: uppercase;\n}\n.jini-chat-pane .jini-composer-discovery-item {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 2px;\n width: 100%;\n padding: 7px 8px;\n color: inherit;\n text-align: start;\n background: transparent;\n border: 0;\n border-radius: 7px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-composer-discovery-item:hover {\n background: var(--jini-chat-panel);\n}\n.jini-chat-pane .jini-composer-discovery-item.is-active {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-accent-soft);\n outline: 1px solid var(--jini-chat-accent);\n outline-offset: -1px;\n box-shadow: inset 3px 0 0 var(--jini-chat-accent);\n}\n.jini-chat-pane .jini-composer-discovery-item small { color: var(--jini-chat-muted); }\n/* The full description used to print under every row unconditionally, which is why the palette\n grew to dozens of multi-line rows on a bare \"/\" \u2014 kept in the accessibility tree at rest (never\n 'display: none', which most screen readers drop from that tree) so 'aria-describedby' keeps\n resolving whether or not anything is hovering.\n FIXED (was: opacity-only toggle, THEN an absolutely-positioned overlay) \u2014 two owner-reported\n bugs in sequence on the same element:\n 1. 'opacity: 0' alone hides a box visually but does not remove it from layout, so an\n absolutely-positioned tooltip sized to its full natural content height (routinely 50-260px\n for a real description) still counted toward its scrolling ancestor's scrollable overflow\n region while invisible ('scrollHeight' measured 501px against a 242px 'clientHeight' for a\n 6-row list), leaving a blank scrollable gap after the last real row. Fixed by collapsing the\n box itself at rest ('max-height: 0; overflow: hidden;'), not just hiding it \u2014 carried\n forward below.\n 2. That fix still left this 'position: absolute; top: 100%' of its own row (formerly anchored\n via 'position: relative' on '.jini-composer-discovery-item' above, now removed \u2014 nothing\n anchors off it anymore): 'z-index' only controls paint order, not which box a box is\n confined to, so on hover/focus the expanded tooltip painted directly over whichever row\n happened to sit below it in the SAME scrollable list (owner report: hovering \"UI/UX Design\n (Skill)\" covered the \"/mcp\" entry beneath it \u2014 not a stacking-order problem, a containment\n one). Fixed by dropping 'position: absolute' entirely: this is now a normal in-flow child of\n the row's own 'flex-direction: column' box, still collapsed to negligible size at rest via\n 'max-height: 0; overflow: hidden;', but on hover/focus it grows the ROW's own height instead\n of floating a detached box over the next row \u2014 the popover's flex column (and every row\n below) gets pushed down to make room, so it can never occlude a sibling item. This also\n retires the panel-styled \"floating card\" look (background/border/shadow): an in-flow caption\n reads correctly with the row's own muted small-text convention above, and dropping it removes\n the last reason this needed its own stacking context. 'pointer-events: none' is kept for\n parity even though nothing sits under it to protect anymore. */\n.jini-chat-pane .jini-composer-discovery-description {\n width: 100%;\n max-height: 0;\n padding: 0;\n overflow: hidden;\n color: var(--jini-chat-muted);\n font-size: 12px;\n font-weight: 400;\n line-height: 1.4;\n white-space: normal;\n opacity: 0;\n pointer-events: none;\n transition: opacity .12s ease;\n}\n.jini-chat-pane .jini-composer-discovery-item:hover .jini-composer-discovery-description,\n.jini-chat-pane .jini-composer-discovery-item:focus-visible .jini-composer-discovery-description {\n max-height: none;\n padding-top: 2px;\n overflow: visible;\n opacity: 1;\n}\n/* '<code>'/'<small>' both default to the browser's UA styling (monospace at an unrelated size)\n with nothing here overriding it \u2014 computed font-SIZE happened to already match the row label\n (13.5px), so the visible mismatch is font-FAMILY: raw monospace next to the label's sans-serif\n stack reads as a different, larger type scale even at equal size. Inheriting the row's own font\n puts both on one consistent scale. */\n.jini-chat-pane .jini-composer-slash-argument,\n.jini-chat-pane .jini-composer-slash-confirm-badge {\n font-family: inherit;\n font-size: inherit;\n font-weight: 400;\n color: var(--jini-chat-muted);\n}\n.jini-chat-pane .jini-composer-file-input {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n clip-path: inset(50%);\n}\n.jini-chat-pane .jini-composer-attach {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n padding: 0;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 1px solid transparent;\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-composer-attach:hover:not(:disabled) {\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border-color: var(--jini-chat-border);\n}\n.jini-chat-pane .jini-composer-attach:disabled { opacity: .45; cursor: default; }\n/* The working-directory trigger button reuses '.jini-composer-attach' itself (Composer.tsx) rather\n than a class duplicated here, so it is byte-identical in size/hit-area/hover/focus to its\n neighbour rather than an approximation that could drift out of sync. Only the wrapper and its\n popover are new. NOT 'position: relative' on the wrapper, for the same reason noted above\n '.jini-composer-discovery': the popover's un-measured first paint (before the layout effect sets\n its 'position: fixed' inline style) falls back to this rule's 'position: absolute', which needs\n '.jini-composer' itself \u2014 the nearest positioned ancestor \u2014 not this wrapper, to anchor against. */\n.jini-chat-pane .jini-composer-workdir { display: inline-flex; }\n.jini-chat-pane .jini-composer-workdir-panel {\n position: absolute;\n z-index: 8;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px;\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 10px;\n box-shadow: 0 12px 30px rgb(0 0 0 / 14%);\n}\n.jini-chat-pane .jini-composer-workdir-input {\n flex: 1;\n min-width: 160px;\n padding: 6px 8px;\n color: var(--jini-chat-text);\n font: inherit;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 6px;\n}\n.jini-chat-pane .jini-composer-workdir-confirm {\n padding: 6px 10px;\n color: white;\n white-space: nowrap;\n background: var(--jini-chat-text-strong);\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n/* `.jini-icon-spin` is `<Icon name=\"spinner\">` (the tool-row \"Running\" badge). Its keyframes used to\n live only in reference.css, which no host imports, so the badge sat still. */\n.jini-composer-spinner,\n.jini-runtime-spinner,\n.jini-icon-spin { animation: jini-chat-spin .8s linear infinite; }\n@keyframes jini-chat-spin { to { transform: rotate(360deg); } }\n.jini-chat-pane .jini-composer-footer-accessories {\n display: flex;\n margin-left: auto;\n min-width: 0;\n}\n.jini-chat-pane .jini-composer-send-group {\n display: flex;\n flex-shrink: 0;\n gap: 6px;\n}\n/* A footer with no attach/accessory controls before the send group (a public visitor chat) would\n leave the lone send button at the start under the footer's space-between; keep it at the end. */\n.jini-chat-pane .jini-composer-send-group:first-child { margin-left: auto; }\n.jini-chat-pane .jini-composer-send {\n display: grid;\n place-items: center;\n width: 32px;\n height: 32px;\n padding: 0;\n color: white;\n background: var(--jini-chat-text-strong);\n border: 0;\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-composer-send:disabled {\n color: #aaa59e;\n background: #ece8e2;\n cursor: default;\n}\n.jini-chat-pane__workdir {\n display: flex;\n align-items: center;\n gap: 7px;\n min-width: 0;\n padding: 5px 4px 0;\n color: var(--jini-chat-muted);\n font-size: 13px;\n}\n.jini-chat-pane__workdir code {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-picker {\n position: relative;\n display: inline-flex;\n align-items: flex-start;\n}\n.jini-chat-pane .jini-working-dir-trigger-row {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n}\n.jini-chat-pane .jini-working-dir-trigger {\n appearance: none;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 6px;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 0;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-working-dir-trigger:hover,\n.jini-chat-pane .jini-working-dir-trigger:focus-visible {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n outline: none;\n}\n.jini-chat-pane .jini-working-dir-trigger.invalid { color: var(--danger); }\n.jini-chat-pane .jini-working-dir-trigger-icon { flex: 0 0 auto; opacity: .75; }\n.jini-chat-pane .jini-working-dir-trigger.invalid .jini-working-dir-trigger-icon {\n color: var(--danger);\n opacity: 1;\n}\n.jini-chat-pane .jini-working-dir-trigger-label {\n max-width: 240px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-trigger-chevron { flex: 0 0 auto; opacity: .55; }\n.jini-chat-pane .jini-working-dir-panel,\n.jini-chat-pane .jini-working-dir-flyout {\n position: absolute;\n z-index: 90;\n display: flex;\n flex-direction: column;\n min-width: 210px;\n padding: 5px;\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border-strong);\n border-radius: 12px;\n box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 10px 28px -10px rgba(0, 0, 0, .18);\n}\n.jini-chat-pane .jini-working-dir-panel {\n top: calc(100% + 6px);\n left: 0;\n}\n.jini-chat-pane .jini-working-dir-panel.up {\n top: auto;\n bottom: calc(100% + 6px);\n}\n.jini-chat-pane .jini-working-dir-item {\n appearance: none;\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n min-height: 32px;\n padding: 0 9px;\n color: var(--jini-chat-text-strong);\n background: transparent;\n border: 0;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n text-align: left;\n cursor: pointer;\n}\n.jini-chat-pane .jini-working-dir-item:hover,\n.jini-chat-pane .jini-working-dir-item:focus-visible,\n.jini-chat-pane .jini-working-dir-recent-item:hover,\n.jini-chat-pane .jini-working-dir-recent-item:focus-visible {\n background: var(--jini-chat-subtle);\n outline: none;\n}\n.jini-chat-pane .jini-working-dir-item-icon { flex: 0 0 auto; opacity: .8; }\n.jini-chat-pane .jini-working-dir-item > span {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-item-chevron { margin-left: auto; opacity: .55; }\n.jini-chat-pane .jini-working-dir-submenu-row { position: relative; }\n.jini-chat-pane .jini-working-dir-flyout {\n top: -5px;\n left: 100%;\n z-index: 91;\n min-width: 220px;\n max-width: 320px;\n margin-left: 4px;\n}\n.jini-chat-pane .jini-working-dir-flyout.up { top: auto; bottom: -5px; }\n.jini-chat-pane .jini-working-dir-flyout::before {\n position: absolute;\n top: 0;\n left: -8px;\n width: 8px;\n height: 100%;\n content: \"\";\n}\n.jini-chat-pane .jini-working-dir-recent-item {\n appearance: none;\n display: grid;\n grid-template-rows: auto auto;\n grid-template-columns: auto 1fr;\n align-items: center;\n width: 100%;\n min-height: 36px;\n padding: 4px 9px;\n color: var(--jini-chat-text-strong);\n background: transparent;\n border: 0;\n border-radius: 8px;\n font: inherit;\n text-align: left;\n cursor: pointer;\n column-gap: 9px;\n}\n.jini-chat-pane .jini-working-dir-recent-item .jini-working-dir-item-icon {\n grid-row: 1 / span 2;\n}\n.jini-chat-pane .jini-working-dir-recent-name,\n.jini-chat-pane .jini-working-dir-recent-path {\n grid-column: 2;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-recent-name { font-size: 13px; }\n.jini-chat-pane .jini-working-dir-recent-path {\n color: var(--jini-chat-muted);\n font-size: 11px;\n opacity: .7;\n}\n.jini-chat-pane .jini-working-dir-empty {\n padding: 8px 10px;\n color: var(--jini-chat-muted);\n font-size: 12px;\n white-space: nowrap;\n opacity: .7;\n}\n.jini-runtime-picker { position: relative; }\n.jini-runtime-trigger {\n display: inline-grid;\n grid-template-columns: 24px 14px;\n align-items: center;\n gap: 7px;\n height: 32px;\n min-width: 0;\n padding: 3px 7px 3px 5px;\n color: var(--jini-chat-text);\n background: transparent;\n border: 0;\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-runtime-trigger:hover { background: var(--jini-chat-subtle); }\n.jini-runtime-trigger__copy { display: none; }\n.jini-runtime-trigger__copy strong {\n overflow: hidden;\n max-width: 122px;\n font-size: 11px;\n font-weight: 650;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-runtime-trigger__copy small {\n overflow: hidden;\n max-width: 122px;\n color: var(--jini-chat-muted);\n font-size: 9px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-runtime-trigger__chevron { color: var(--jini-chat-muted); font-size: 12px; }\n.jini-runtime-agent-icon.agent-icon { flex: 0 0 auto; object-fit: contain; }\n.jini-runtime-agent-icon.agent-icon-mono {\n display: inline-block;\n background-color: currentColor;\n -webkit-mask-repeat: no-repeat;\n mask-repeat: no-repeat;\n -webkit-mask-position: center;\n mask-position: center;\n -webkit-mask-size: contain;\n mask-size: contain;\n}\n.jini-runtime-agent-icon.agent-icon-fallback {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #74716b;\n background: #f4f5f7;\n border: 1px solid #e1e5eb;\n border-radius: 6px;\n font-weight: 700;\n}\n.jini-runtime-popover {\n --jini-chat-panel: #fdfcfa;\n --jini-chat-text: #1a1916;\n --jini-chat-text-strong: #0d0c0a;\n --jini-chat-muted: #74716b;\n --jini-chat-faint: #b3b0a8;\n --jini-chat-border: #e1e5eb;\n --jini-chat-border-soft: #edf0f4;\n --jini-chat-subtle: #f4f5f7;\n box-sizing: border-box;\n padding: 8px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Microsoft YaHei UI\", \"Noto Sans\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n font-size: 13.5px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n}\n.jini-runtime-popover__head {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 10px 12px 12px;\n border-bottom: 1px solid var(--jini-chat-border-soft);\n}\n.jini-runtime-popover__head strong { color: var(--jini-chat-text-strong); font-size: 14px; font-weight: 650; }\n.jini-runtime-popover__head span { color: var(--jini-chat-muted); font-size: 12px; }\n.jini-runtime-mode,\n.jini-runtime-agent,\n.jini-runtime-rescan {\n display: flex;\n align-items: center;\n gap: 11px;\n box-sizing: border-box;\n width: 100%;\n min-height: 40px;\n padding: 8px 11px;\n color: var(--jini-chat-text);\n background: transparent;\n border: 0;\n border-radius: 8px;\n text-align: left;\n cursor: pointer;\n}\n.jini-runtime-mode:hover,\n.jini-runtime-agent:hover:not(:disabled),\n.jini-runtime-rescan:hover:not(:disabled) { background: var(--jini-chat-subtle); }\n.jini-runtime-mode.is-active { font-weight: 650; }\n.jini-runtime-mode:disabled,\n.jini-runtime-agent:disabled,\n.jini-runtime-rescan:disabled { color: var(--jini-chat-faint); cursor: default; }\n.jini-runtime-mode__meta,\n.jini-runtime-agent__status {\n margin-left: auto;\n color: var(--jini-chat-muted);\n font-size: 12px;\n white-space: nowrap;\n}\n.jini-runtime-check { flex: 0 0 auto; color: var(--jini-chat-text); }\n/* Matches .jini-message-attachment-chip/.jini-md-table-expand's pill chrome (same\n background/border/radius formula) so this reads as the same badge language already used\n elsewhere in the pane, not a new one. Sits between the agent name and its status column \u2014\n flex: 0 0 auto keeps it from stretching or shrinking when the row is tight. */\n.jini-runtime-agent__badge {\n flex: 0 0 auto;\n padding: 2px 8px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n font-size: 10px;\n font-weight: 500;\n white-space: nowrap;\n}\n.jini-runtime-section-label {\n padding: 12px 11px 5px;\n color: var(--jini-chat-faint);\n font-size: 11px;\n font-weight: 650;\n letter-spacing: .08em;\n text-transform: uppercase;\n}\n.jini-runtime-agent.is-active { background: transparent; }\n.jini-runtime-agent__copy {\n display: flex;\n flex: 1;\n flex-direction: column;\n min-width: 0;\n}\n.jini-runtime-agent__copy strong { font-size: 13.5px; font-weight: 500; }\n.jini-runtime-agent__copy small {\n overflow: hidden;\n color: #8b857e;\n font-size: 10px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-runtime-empty { padding: 10px 11px; color: var(--jini-chat-muted); font-size: 12px; }\n.jini-runtime-models {\n display: flex;\n flex-direction: column;\n gap: 9px;\n margin-top: 5px;\n padding: 10px 11px 8px;\n border-top: 1px dashed var(--jini-chat-border);\n}\n.jini-runtime-select {\n display: flex;\n flex-direction: column;\n gap: 5px;\n color: var(--jini-chat-muted);\n font-size: 13px;\n}\n.jini-runtime-select select {\n box-sizing: border-box;\n width: 100%;\n min-height: 40px;\n padding: 7px 10px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n}\n/*\n * The BYOK model, rendered as a value rather than a control \u2014 see RuntimeByokDetails for why it\n * is not a disabled select. It occupies the same box the sibling selects do so the row still\n * reads as part of the same list, but carries no border: a bordered box with no affordance is\n * exactly the \"looks editable, isn't\" shape the plain text exists to avoid.\n */\n/*\n * The BYOK model list, which CustomSelect portals to document.body. runtimePopoverPosition puts\n * the runtime popover at z-index 1000; the shared .jini-select-menu default is 60, which is right\n * everywhere else and puts this menu behind the very popover that opened it. One above the\n * popover, not far above: it must clear its own surface and nothing else.\n */\n.jini-select-menu.jini-runtime-model-menu {\n z-index: 1001;\n}\n.jini-runtime-byok-model strong {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n min-height: 40px;\n padding: 7px 10px;\n color: var(--jini-chat-text);\n font-weight: 600;\n word-break: break-all;\n}\n/*\n * Tool-call cards (ToolCard.tsx) and the per-turn usage summary (MessageRow.tsx) ship no CSS of\n * their own \u2014 see this file's own header note on why the package supplies a default theme at all.\n * Collapsed by default (.op-card-head is the only always-visible part), so a run with a dozen tool\n * calls reads as a dozen one-line rows, not a page of raw JSON.\n */\n.jini-chat-pane .jini-message-tools {\n display: flex;\n flex-direction: column;\n gap: 6px;\n width: 100%;\n}\n.jini-chat-pane .op-card {\n border: 1px solid var(--jini-chat-border);\n border-radius: var(--radius);\n background: var(--jini-chat-subtle);\n overflow: hidden;\n font-size: 13px;\n}\n.jini-chat-pane .op-card-head {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 7px 10px;\n background: none;\n border: 0;\n font: inherit;\n text-align: left;\n cursor: pointer;\n color: inherit;\n}\n.jini-chat-pane .op-status { display: inline-flex; flex-shrink: 0; }\n.jini-chat-pane .op-status-ok { color: #4e875f; }\n.jini-chat-pane .op-status-error { color: var(--danger); }\n.jini-chat-pane .op-status-running { color: var(--jini-chat-accent); }\n.jini-chat-pane .op-title { font-weight: 600; flex-shrink: 0; }\n.jini-chat-pane .shimmer-text { animation: jini-chat-shimmer 1.6s ease-in-out infinite; }\n@keyframes jini-chat-shimmer { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }\n@media (prefers-reduced-motion: reduce) { .jini-chat-pane .shimmer-text { animation: none; opacity: .6; } }\n.jini-chat-pane .op-meta {\n color: var(--jini-chat-muted);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n min-width: 0;\n}\n.jini-chat-pane .op-expand-chev { flex-shrink: 0; color: var(--jini-chat-faint); }\n/* Collapsed by default: 0 max-height clips the inner content, no display:none \u2014 keeps the height\n transition instead of an instant jump cut. */\n.jini-chat-pane .accordion-collapsible { max-height: 0; overflow: hidden; transition: max-height .15s ease; }\n.jini-chat-pane .accordion-collapsible.open { max-height: 480px; overflow-y: auto; }\n.jini-chat-pane .accordion-collapsible-inner { padding: 0 10px 10px; }\n.jini-chat-pane .op-card-detail { display: flex; flex-direction: column; gap: 6px; }\n.jini-chat-pane .op-path,\n.jini-chat-pane .op-command,\n.jini-chat-pane .op-output {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 6px;\n padding: 6px 8px;\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n}\n/* The \"Waiting for the result or answer\u2026\" / \"Finished with no output.\" line in an expanded tool row (ToolCard.tsx's ToolCallDetail). */\n.jini-chat-pane .op-detail-note { margin: 0; font-size: 12px; color: var(--jini-chat-muted); }\n.jini-chat-pane .op-open {\n border: 1px solid var(--jini-chat-border-strong);\n border-radius: 6px;\n background: var(--jini-chat-panel);\n padding: 2px 8px;\n font-size: 11px;\n cursor: pointer;\n}\n/* The \"Done \u00B7 6m 29s \u00B7 2612 out \u00B7 $0.4028\" line \u2014 real numbers from the run's own kind:'usage' event. */\n.jini-chat-pane .jini-message-usage {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n color: var(--jini-chat-muted);\n margin-top: 2px;\n}\n.jini-chat-pane .jini-message-usage-dot { font-size: 8px; color: #4e875f; }\n/* toolCallDisplay 'collapsed' (ToolCallGroup.tsx): one quiet line per turn, cards open under it. */\n.jini-chat-pane .jini-tool-group { display: flex; flex-direction: column; gap: 6px; width: 100%; }\n.jini-chat-pane .jini-tool-group__toggle {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n align-self: flex-start;\n max-width: 100%;\n padding: 2px 0;\n background: none;\n border: 0;\n font: inherit;\n font-size: 12px;\n color: var(--jini-chat-muted);\n cursor: pointer;\n}\n.jini-chat-pane .jini-tool-group__toggle:hover { color: inherit; }\n.jini-chat-pane .jini-tool-group__toggle:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 2px; border-radius: 4px; }\n.jini-chat-pane .jini-tool-group__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.jini-chat-pane .jini-tool-group[data-failed=\"true\"] .jini-tool-group__toggle { color: var(--danger); }\n/* showThinking (MessageRow.tsx's ThinkingBlock): the model's reasoning, folded, set apart from the answer. */\n.jini-chat-pane .jini-message-thinking { font-size: 12px; color: var(--jini-chat-muted); }\n.jini-chat-pane .jini-message-thinking > summary { cursor: pointer; width: fit-content; }\n.jini-chat-pane .jini-message-thinking__text {\n margin-top: 4px;\n padding-left: 10px;\n border-left: 2px solid var(--jini-chat-border);\n white-space: pre-wrap;\n word-break: break-word;\n}\n/*\n * Fenced code block ('Markdown.tsx''s 'renderBlock' case 'code') and inline 'code' span visual\n * treatment \u2014 previously entirely unstyled (bare UA-default '<pre><code>', no background, border,\n * or padding; a downstream admin host's own stylesheet already flagged this gap in a note next to\n * its unrelated horizontal-scroll fix for the same element). Lives HERE, in the package's own\n * injected default theme, rather than in a host's stylesheet: a host CAN already remap the exact\n * '--jini-chat-*' custom properties this rule reads (one such host's '.admin-chat-dock .jini-chat-\n * pane' block already does, for every other rule in this file), so putting the fix here makes every\n * Jini host look correct by default AND lets a host match its own palette for free, with no new\n * host-side override rule needed at all \u2014 the reverse (host-only CSS) would leave every other\n * embedder of '@jini-ai/chat' with the same unstyled '<pre>' this fix exists to correct.\n * 'max-width'/'overflow-x' here duplicate (harmlessly) a narrower fix that same host's own\n * '.jini-message-content pre' rule already carries for its own fixed-width dock specifically \u2014 this\n * package cannot assume every host has that rule, so a real host-agnostic default belongs here too.\n * NOTE: this comment avoids backtick quoting on purpose \u2014 this whole stylesheet lives inside a JS\n * template literal, so a literal backtick here would terminate the string, not just this comment.\n */\n.jini-chat-pane .jini-message-content pre {\n margin: 6px 0;\n max-width: 100%;\n overflow-x: auto;\n padding: 10px 12px;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12.5px;\n line-height: 1.5;\n}\n.jini-chat-pane .jini-message-content pre code {\n padding: 0;\n background: none;\n border: 0;\n color: inherit;\n font: inherit;\n}\n.jini-chat-pane .jini-message-content :not(pre) > code {\n padding: .15em .4em;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 4px;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: .9em;\n}\n/*\n * Copy affordance for a fenced code block ('Markdown.tsx''s 'CodeBlock'/'CopyCodeButton'). The\n * button lives in its own toolbar row fused to the TOP of the '<pre>' below, not overlaid on top\n * of the code itself \u2014 an overlay would sit over whatever text happens to render at that corner,\n * and '<pre>' already carries its own horizontal scrollbar along its own bottom edge, so a row\n * above it can never collide with that scrollbar either. '.jini-md-code-block' takes over the\n * '<pre>' rule's own top/bottom margin above so the two don't stack, and the toolbar/pre pair\n * share one rounded box (toolbar keeps the top corners, '<pre>' below loses them) instead of two\n * separately-bordered boxes stacked on each other.\n */\n.jini-chat-pane .jini-message-content .jini-md-code-block {\n margin: 6px 0;\n}\n.jini-chat-pane .jini-message-content .jini-md-code-block pre {\n margin: 0;\n border-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.jini-chat-pane .jini-md-code-toolbar {\n display: flex;\n justify-content: flex-end;\n padding: 3px;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-bottom: 0;\n border-radius: 8px 8px 0 0;\n}\n.jini-chat-pane .jini-md-code-copy {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n padding: 0;\n color: var(--jini-chat-faint);\n background: none;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-md-code-copy:hover { color: var(--jini-chat-muted); background: var(--jini-chat-border); }\n.jini-chat-pane .jini-md-code-copy:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 1px; }\n/* Visually hidden but present in the DOM (never display:none) so this polite live region still\n gets announced \u2014 same clip-rect technique 'jini-message-copy-status' above already uses. */\n.jini-chat-pane .jini-md-code-copy-status {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n}\n/*\n * GFM pipe tables ('Markdown.tsx''s 'TableBlock') \u2014 same reasoning as the code-block rules directly\n * above for living in the package's own default theme rather than a host stylesheet. '.jini-md-\n * table-wrap' gets its own horizontal scrollbar so a wide table scrolls internally instead of\n * dragging '.jini-message-list' sideways with it (the exact failure mode that same downstream host's\n * stylesheet documents for the code-block case \u2014 the same fix shape applies here). The \"Expand\n * table\" button only renders ('TableBlock''s own logic) once the table has genuinely overflowed\n * that wrap.\n */\n.jini-md-table-wrap {\n max-width: 100%;\n overflow-x: auto;\n}\n.jini-md-table {\n border-collapse: collapse;\n font-size: 12.5px;\n}\n/*\n * 'min-width' here, not on '.jini-md-table' itself, and picked per column rather than as one flat\n * table-wide number -- a flat table minimum sized for a typical 2-column table would leave a\n * 5-column table crushed just as badly as before (same failure this comment's neighbor above\n * describes), while one sized for 5 columns would force pointless overflow on a simple 2-column\n * table. Per-cell 'min-width' scales the effective table floor with the real column count instead.\n * 130px was picked empirically, not guessed: measured (real Chromium, not jsdom -- 'scrollWidth'/\n * 'clientWidth' are unimplemented there) against this package's narrowest known real host, an\n * admin chat dock fixed at 380px wide ('admin-chat-dock' in that host's own stylesheet), which\n * nets out to roughly 238px of usable width once that dock's own message-list padding, the 80%\n * message-bubble cap, and this content's own padding are all applied. Below 120px per column the\n * table still fits inside those 238px and never overflows -- the exact bug this fix exists to\n * correct. 130px was chosen just above that measured 120px crossover (a small margin so it clears\n * reliably rather than sitting exactly on the boundary) and reads as a genuinely defensible per-\n * column floor on its own terms too: with this rule's own 6px/10px cell padding subtracted, it\n * leaves about 110px of text room, roughly 14-16 characters per wrapped line at this rule's 12.5px\n * font -- enough that a table cell wraps in short phrases instead of one word per line, which is\n * the readability floor this fix is actually chasing. One real consequence, verified rather than\n * hand-waved: at this width, ANY 2-column table in that narrow dock now overflows and gets the\n * \"Expand table\" affordance, including ones with short cell content -- there is no per-cell\n * min-width value that can overflow a long-prose 2-column table without also overflowing a short\n * one at the same column count, because the browser's own auto-layout table-width algorithm sizes\n * the table to the greater of its container's width and the sum of each column's own minimum --\n * content length past that minimum only changes how much a column wraps internally, never whether\n * the table exceeds its wrap. Accepted here: in a surface already this narrow, showing \"Expand\n * table\" a little more eagerly is the better failure mode than silently crushing a table's own\n * content illegibly, which is the state this whole rule exists to fix.\n */\n.jini-md-table th,\n.jini-md-table td {\n padding: 6px 10px;\n min-width: 130px;\n border: 1px solid var(--jini-chat-border);\n text-align: left;\n}\n.jini-md-table th {\n background: var(--jini-chat-subtle);\n color: var(--jini-chat-text-strong);\n font-weight: 650;\n}\n.jini-md-table-expand {\n margin-top: 4px;\n padding: 4px 10px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n font-size: 11px;\n cursor: pointer;\n}\n.jini-md-table-expand:hover { color: var(--jini-chat-text-strong); }\n.jini-md-table-modal {\n max-width: min(90vw, 900px);\n max-height: 80vh;\n overflow: auto;\n padding: 20px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06);\n}\n.jini-md-table-modal::backdrop {\n background: rgba(0, 0, 0, .4);\n}\n.jini-md-table-modal-close {\n position: absolute;\n top: 10px;\n right: 10px;\n width: 28px;\n height: 28px;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 0;\n border-radius: 6px;\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n}\n.jini-md-table-modal-close:hover {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n}\n@media (max-width: 560px) {\n .jini-chat-pane__header { padding-inline: 16px; }\n .jini-chat-pane .jini-message-list { padding-inline: 16px; }\n .jini-chat-pane__controls { padding-inline: 10px; }\n}\n/*\n * Attachment preview modal (MessageRow.tsx's chip -> AttachmentPreviewModal.tsx). Same\n * native-<dialog> shape as .jini-md-table-modal above (own box sized to content, not viewport;\n * ::backdrop dims the page), kept as its own class rather than reusing that one because the two\n * differ in intent: a table pop-out is a fixed-chrome expansion of content already on screen, this\n * is a general image/text/metadata viewer with its own header and a dedicated close control.\n */\n.jini-attachment-preview-dialog {\n display: flex;\n flex-direction: column;\n width: min(90vw, 720px);\n max-height: 80vh;\n padding: 0;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06);\n}\n.jini-attachment-preview-dialog::backdrop {\n background: rgba(0, 0, 0, .4);\n}\n.jini-attachment-preview-header {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--jini-chat-border-soft);\n}\n.jini-attachment-preview-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 650;\n color: var(--jini-chat-text-strong);\n}\n.jini-attachment-preview-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n width: 26px;\n height: 26px;\n margin-left: auto;\n padding: 0;\n color: var(--jini-chat-muted);\n background: none;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.jini-attachment-preview-close:hover {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n}\n.jini-attachment-preview-body {\n display: flex;\n flex: 1;\n min-height: 0;\n align-items: center;\n justify-content: center;\n padding: 16px;\n overflow: auto;\n}\n.jini-attachment-preview-image {\n display: block;\n max-width: 100%;\n max-height: calc(80vh - 90px);\n object-fit: contain;\n margin: auto;\n}\n.jini-attachment-preview-text {\n width: 100%;\n max-height: calc(80vh - 90px);\n margin: 0;\n padding: 12px;\n overflow: auto;\n color: var(--jini-chat-text);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border-soft);\n border-radius: 8px;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.jini-attachment-preview-truncated {\n width: 100%;\n margin-top: 8px;\n color: var(--jini-chat-muted);\n font-size: 11px;\n text-align: center;\n}\n.jini-attachment-preview-loading {\n color: var(--jini-chat-muted);\n font-size: 13px;\n font-style: italic;\n}\n.jini-attachment-preview-meta {\n display: flex;\n flex-direction: column;\n gap: 6px;\n width: 100%;\n max-width: 360px;\n color: var(--jini-chat-text);\n font-size: 13px;\n}\n.jini-attachment-preview-meta-row {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n}\n.jini-attachment-preview-meta-label {\n color: var(--jini-chat-faint);\n}\n.jini-attachment-preview-meta-note {\n margin-top: 6px;\n color: var(--jini-chat-muted);\n font-size: 12px;\n}\n/* Previous/next across a message's (or the composer's) images: a footer under the image, so the\n * buttons sit on the panel colour - readable over any picture - side by side in the bottom-right. */\n/* A fixed stage while stepping: sized to each image, the dialog would resize and move the buttons\n * out from under the pointer between clicks. */\n.jini-attachment-preview-dialog.has-nav .jini-attachment-preview-body {\n flex: 0 0 auto;\n height: min(calc(80vh - 152px), 480px);\n}\n.jini-attachment-preview-dialog.has-nav .jini-attachment-preview-image,\n.jini-attachment-preview-dialog.has-nav .jini-attachment-preview-text {\n max-height: 100%;\n}\n.jini-attachment-preview-nav {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 10px 14px;\n border-top: 1px solid var(--jini-chat-border-soft);\n}\n.jini-attachment-preview-count {\n margin-right: auto;\n color: var(--jini-chat-muted);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n}\n.jini-attachment-preview-step {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n padding: 0;\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-attachment-preview-step:hover {\n background: var(--jini-chat-subtle);\n border-color: var(--jini-chat-border-strong);\n}\n.jini-attachment-preview-step:focus-visible {\n outline: 2px solid var(--jini-chat-accent);\n outline-offset: 2px;\n}\n";
|
|
1
|
+
export declare const CHAT_PANE_STYLES = "\n.jini-chat-pane {\n --jini-chat-bg: #faf9f7;\n --jini-chat-panel: #fdfcfa;\n --jini-chat-text: #1a1916;\n --jini-chat-text-strong: #0d0c0a;\n --jini-chat-muted: #74716b;\n --jini-chat-faint: #b3b0a8;\n --jini-chat-border: #e1e5eb;\n --jini-chat-border-strong: #c9d0da;\n --jini-chat-border-soft: #edf0f4;\n --jini-chat-subtle: #f4f5f7;\n --jini-chat-accent: #c96442;\n --jini-chat-accent-soft: #fbeee5;\n --jini-chat-danger: #e5484d;\n --jini-chat-radius: 8px;\n --jini-chat-radius-lg: 12px;\n --bg-panel: var(--jini-chat-panel);\n --bg-subtle: var(--jini-chat-subtle);\n --border: var(--jini-chat-border);\n --border-strong: var(--jini-chat-border-strong);\n --text: var(--jini-chat-text);\n --text-strong: var(--jini-chat-text-strong);\n --text-muted: var(--jini-chat-muted);\n --danger: var(--jini-chat-danger);\n --radius: var(--jini-chat-radius);\n --radius-lg: var(--jini-chat-radius-lg);\n position: relative;\n display: grid;\n grid-template-rows: auto minmax(0, 1fr) auto;\n min-width: 0;\n min-height: 0;\n height: 100%;\n /* clip after the hidden fallback: a hidden box still scrolls programmatically (a focus() or\n scrollIntoView() inside it), which shifts the pinned composer up and leaves blank pane below. */\n overflow: hidden;\n overflow: clip;\n color: var(--jini-chat-text);\n background: var(--jini-chat-bg);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Microsoft YaHei UI\", \"Noto Sans\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n font-size: 13.5px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.jini-chat-pane button,\n.jini-runtime-popover button,\n.jini-runtime-popover select {\n font: inherit;\n}\n.jini-chat-pane__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n min-height: 74px;\n padding: 18px 22px 14px;\n border-bottom: 1px solid var(--jini-chat-border-soft);\n}\n.jini-chat-pane__heading { min-width: 0; }\n.jini-chat-pane__eyebrow {\n display: block;\n margin-bottom: 4px;\n color: var(--jini-chat-muted);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 10px;\n letter-spacing: .1em;\n text-transform: uppercase;\n}\n.jini-chat-pane__title {\n overflow: hidden;\n margin: 0;\n font-size: 21px;\n font-weight: 650;\n letter-spacing: -.025em;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane__new-thread {\n padding: 8px 12px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-chat-pane__body {\n position: relative;\n min-height: 0;\n overflow: hidden;\n overflow: clip;\n}\n.jini-chat-pane .jini-message-list {\n height: 100%;\n overflow-y: auto;\n /*\n * .jini-chat-pane__controls is an absolutely-positioned overlay (not a flex/grid sibling that\n * pushes this list up), so its real height has to be reserved here instead. This used to be a\n * flat 240px guess -- and had already gone stale once before that: it started at 170px, a live\n * render outgrew it (composer + footer + working-directory row alone measured 179.75px, clipping\n * the last message's tail under the overlay), and 240px was chosen as a margin above THAT\n * measurement, not a value anything keeps in sync. useChatPaneControlsHeight now measures the\n * overlay's real height live via ResizeObserver and publishes it as\n * --jini-chat-controls-height on the pane root, so this reservation tracks the composer's\n * actual content (an attachment tray, a wrapped multi-line draft, a taller runtime-picker\n * popover) instead of a number that is only ever correct until the next thing that grows it.\n * 240px survives as the fallback for the rare environment without ResizeObserver (SSR; a test\n * harness that never polyfills it) -- everywhere else, the calc() wins.\n */\n padding: 20px 22px calc(var(--jini-chat-controls-height, 240px) + 24px);\n scrollbar-width: thin;\n}\n.jini-chat-pane .jini-message {\n margin-bottom: 20px;\n color: var(--jini-chat-text);\n font-size: 14px;\n line-height: 1.62;\n}\n/*\n * Off-screen rows skip style, layout and paint. Owner report 2026-10-11: typing in a long chat\n * lagged. React was not the cost (no row re-renders per key); the browser was. A keystroke's DOM\n * change can invalidate style for the whole document (a host dock's :has() rule did),\n * and a transcript with ~2,000 tool cards and 3.5 MB of tool output then cost ~50 ms of style\n * recalc per key. With this, only the rows on screen are recalculated (~8 ms). The DOM stays\n * whole: find-in-page, textContent readers and screen readers still see every row.\n * 'auto' in contain-intrinsic-size keeps a row's last real height once it has rendered; the\n * auto-scroll ResizeObserver re-sticks to the bottom as estimated rows resolve.\n * Containment clips paint at the row's edge, so overflow-clip-margin leaves room for focus rings\n * (2px outline + 2px offset); @supports keeps browsers without it on the old, unclipped path.\n */\n@supports (overflow-clip-margin: 8px) {\n .jini-chat-pane .jini-message {\n content-visibility: auto;\n contain-intrinsic-size: auto 160px;\n overflow-clip-margin: 8px;\n }\n}\n/* A mid-run user bubble is nested inside an assistant turn, so the preceding tool/text block\n * has no message-row bottom margin. Give it the same separation as an ordinary next turn. */\n.jini-chat-pane .jini-message-user--mid-run {\n margin-top: 20px;\n}\n/*\n * Bubble on the content, not the row: MessageRow.tsx renders attachments as siblings of\n * .jini-message-content inside .jini-message-user, each with their own chip styling -- bubbling\n * the whole row would nest one background box inside another. This replaces a dead rule this file\n * shipped with (.jini-message-row[data-role=\"user\"] / .jini-message-row.user): MessageRow.tsx\n * has never rendered a jini-message-row class or a data-role attribute, and a user message's\n * root class is jini-message-user, not bare \"user\" -- the selector never matched anything real.\n */\n.jini-chat-pane .jini-message-user .jini-message-content {\n margin-left: auto;\n padding: 10px 13px;\n width: fit-content;\n max-width: 88%;\n /* User bubbles share this style at every nesting depth, regardless of message length. */\n background: var(--jini-chat-user-bubble, #1a1b1c);\n color: var(--jini-chat-user-bubble-ink, #fff);\n border-radius: 12px 12px 3px 12px;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachments {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n justify-content: flex-end;\n margin-bottom: 4px;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip {\n font: inherit;\n font-size: 11px;\n line-height: 1.4;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n padding: 3px 10px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip:hover {\n color: var(--jini-chat-text);\n border-color: var(--jini-chat-border-strong);\n}\n/* A sent image this browser still holds (SentAttachment.tsx): a small square thumbnail instead of\n * the filename pill. Same click target and preview modal; cover-cropped so mixed aspect ratios line up. */\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip.is-thumbnail {\n width: 64px;\n height: 64px;\n padding: 0;\n overflow: hidden;\n border-radius: 10px;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip.is-thumbnail:focus-visible {\n outline: 2px solid var(--jini-chat-accent);\n outline-offset: 2px;\n}\n.jini-chat-pane .jini-message-attachment-thumb {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n.jini-chat-pane .jini-message-agent {\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .02em;\n text-transform: uppercase;\n color: var(--jini-chat-faint);\n margin-bottom: 4px;\n}\n.jini-chat-pane .jini-message-error { color: var(--danger); font-size: 13px; }\n.jini-chat-pane .jini-message-pending { color: var(--jini-chat-muted); font-size: 13px; font-style: italic; }\n/*\n * The pending-turn strip: a composer-driven turn typed while a run is already streaming, rendered\n * as the newest entry in the transcript (MessageList.tsx's pendingPrompt prop) rather than a\n * banner bolted above the composer \u2014 matching Claude Code/ChatGPT's own pending-message UX\n * (previously ChatPane.tsx's ChatPaneQueuedPrompt, which rendered between the drop-target's drag\n * announcement and the composer; that component is gone).\n *\n * Owner's own framing was \"why isn't the queue message just put in the chat\" \u2014 so\n * .jini-chat-pane__queued-text below is deliberately NOT a variant banner shape; it copies\n * .jini-message-user .jini-message-content's bubble geometry line for line (width: fit-content,\n * max-width: 88%, same padding/border-radius) so it reads as the operator's own message, not a\n * system notice. What marks it as NOT YET SENT is two properties a real sent bubble never carries:\n * a dashed border (a sent bubble has none at all) and reduced opacity \u2014 chosen over the old boxed\n * strip-plus-label treatment because those two are legible at a glance without adding a second\n * visual language next to the transcript's own. The former \"QUEUED\" chip label is gone for the same\n * reason: once the bubble itself reads as pending, a label restates it. role=\"status\" plus the\n * aria-label on the row (MessageList.tsx) still carry that meaning to assistive tech that can't see\n * the dashed border. The cancel action is a separate, smaller line below the bubble (mirroring\n * .jini-message-actions--user's own placement under a real message) rather than inline inside it,\n * so it reads as secondary to the bubble, not competing with it.\n *\n * Reuses this theme's own tokens rather than the currentColor/color-mix approach reference.css uses\n * for the same rules \u2014 that file is deliberately token-free because it ships unstyled and undocked\n * from any root that defines custom properties, but this stylesheet's .jini-chat-pane root already\n * defines concrete --jini-chat-* values for every other rule here, so matching that (not\n * reference.css's host-agnostic fallback) is the actual local convention.\n * NOTE: this comment avoids backtick quoting on purpose, same as the cancel-button comment below \u2014\n * the whole stylesheet lives inside a JS template literal, so a literal backtick here would\n * terminate the string, not just this comment.\n */\n.jini-chat-pane__queued {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 4px;\n margin: 4px 0 0;\n}\n.jini-chat-pane__queued-text {\n width: fit-content;\n max-width: 88%;\n padding: 10px 13px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-subtle);\n border: 1px dashed var(--jini-chat-border);\n border-radius: 12px 12px 3px 12px;\n opacity: .68;\n overflow-wrap: break-word;\n}\n.jini-chat-pane__queued-actions {\n display: flex;\n justify-content: flex-end;\n}\n/*\n * A plain-text cancel affordance below the pending bubble, not a primary action \u2014 without this\n * reset a host's own global 'button' styling (background, border, padding, border-radius, bold\n * text) paints it as a full pill-shaped button that visually competes with (and can overlap) the\n * transcript around it \u2014 the exact failure an operator hit live in one host. '.jini-chat-pane\n * button { font: inherit }' near the top of this file already resets font-family/line-height/etc\n * to this pane's own ambient values; font-size/font-weight are redeclared here (not just left at\n * that inherited ambient size) to size this specific control down to a caption scale, well under\n * the bubble's own 14px text.\n * NOTE: this comment avoids backtick quoting on purpose (see this file's own precedent further\n * down) \u2014 the whole stylesheet lives inside a JS template literal, so a literal backtick here\n * would terminate the string, not just this comment.\n */\n.jini-chat-pane__queued-cancel {\n padding: 2px 6px;\n color: var(--jini-chat-muted);\n background: none;\n border: 0;\n border-radius: 6px;\n font-size: 11px;\n font-weight: 400;\n cursor: pointer;\n}\n.jini-chat-pane__queued-cancel:hover {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-border);\n}\n/*\n * Per-message action row (MessageRow.tsx's 'CopyMessageButton', Icon.tsx's 'copy' glyph): quiet,\n * icon-only, always present rather than a hover-only reveal, so keyboard and touch users get the\n * same access a mouse hover would give. The assistant variant carries a thin left rail (its own\n * left border) to read as attached to the turn above it without repeating that message's own\n * chrome; the user variant skips the rail and instead mirrors its bubble's own right alignment.\n * Copy is the only live button today \u2014 'jini-message-actions--assistant' is an intentional\n * extension seam, not the finished row; see that row's own call-site comment in MessageRow.tsx.\n */\n.jini-chat-pane .jini-message-actions {\n display: flex;\n align-items: center;\n gap: 2px;\n margin-top: 6px;\n}\n.jini-chat-pane .jini-message-actions--assistant {\n padding-left: 8px;\n border-left: 2px solid var(--jini-chat-border-soft);\n}\n.jini-chat-pane .jini-message-actions--user { justify-content: flex-end; }\n.jini-chat-pane .jini-message-action-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n padding: 0;\n color: var(--jini-chat-faint);\n background: none;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-message-action-btn:hover { color: var(--jini-chat-muted); background: var(--jini-chat-subtle); }\n.jini-chat-pane .jini-message-action-btn:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 1px; }\n/* Visually hidden but present in the DOM (never display:none) so the polite live region beside\n each copy button still gets announced \u2014 same clip-rect technique this package already uses for\n its other visually-hidden-but-functional controls. */\n.jini-chat-pane .jini-message-copy-status {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n}\n.jini-chat-pane__controls {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 4;\n padding: 0 16px 14px;\n background: linear-gradient(to bottom, transparent, var(--jini-chat-bg) 22px);\n}\n.jini-chat-pane__suggestions {\n display: flex;\n gap: 8px;\n margin: 0 2px 8px;\n overflow-x: auto;\n}\n.jini-chat-pane__suggestion {\n flex: 0 0 auto;\n max-width: 290px;\n overflow: hidden;\n padding: 7px 11px;\n color: var(--jini-chat-muted);\n background: color-mix(in srgb, var(--jini-chat-panel) 92%, transparent);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n font-size: 11px;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n}\n.jini-chat-pane__error {\n margin: 0 2px 8px;\n padding: 8px 10px;\n color: #8c2f20;\n background: #fff1ed;\n border: 1px solid #f2c8bb;\n border-radius: 8px;\n font-size: 12px;\n}\n.jini-chat-pane__error-action {\n padding: 0;\n color: inherit;\n background: none;\n border: 0;\n font: inherit;\n font-weight: 650;\n text-decoration: underline;\n cursor: pointer;\n}\n.jini-chat-pane__error-action:hover { opacity: .82; }\n.jini-chat-pane__status {\n margin: 0 2px 8px;\n padding: 8px 10px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n font-size: 12px;\n}\n.jini-chat-pane__drop-target {\n position: relative;\n margin: -8px -8px -8px;\n padding: 6px;\n border: 2px dashed transparent;\n border-radius: 16px;\n transition:\n background-color .14s ease,\n border-color .14s ease,\n box-shadow .14s ease;\n}\n.jini-chat-pane__drop-target.is-dragging-files {\n background: color-mix(in srgb, var(--jini-chat-accent-soft) 72%, transparent);\n border-color: var(--jini-chat-accent);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--jini-chat-accent) 15%, transparent);\n}\n.jini-chat-pane__drop-target.is-dragging-files .jini-composer {\n background: color-mix(in srgb, var(--jini-chat-accent-soft) 58%, var(--jini-chat-panel));\n border-color: color-mix(in srgb, var(--jini-chat-accent) 68%, var(--jini-chat-border));\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--jini-chat-accent) 10%, transparent);\n}\n.jini-chat-pane__drop-announcement {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n clip-path: inset(50%);\n}\n.jini-chat-pane .jini-composer {\n position: relative;\n overflow: visible;\n padding: 0;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: none;\n transition: border-color .16s ease, box-shadow .16s ease;\n}\n.jini-chat-pane .jini-composer:focus-within {\n background: var(--jini-chat-panel);\n border-color: color-mix(in srgb, var(--jini-chat-accent) 22%, var(--jini-chat-border-strong));\n box-shadow: 0 1px 2px rgba(26, 25, 22, .06), 0 0 0 1px rgba(201, 100, 66, .06);\n}\n/*\n * Pinned-context zone: whatever a host pins above the composer input (selected plugins, MCP\n * servers, or anything a future host adds -- see 'leadingAccessories''s own doc in 'slots.ts').\n * 'Composer.tsx' only mounts this element when 'slots.leadingAccessories' is a truthy prop value,\n * but a host that always supplies a TRAY COMPONENT rather than conditionally supplying the prop\n * itself (one that internally renders 'null' when nothing is pinned) still leaves an empty node\n * behind -- ':empty' below is what actually collapses THAT case to zero height and zero seam,\n * mirroring '.jini-attachment-tray:empty' a few rules down, which solves the identical problem for\n * the real file-attachment tray.\n *\n * Collapsed (nothing pinned): zero height, zero padding, invisible (0-width) seam -- no reserved\n * space, matching the owner's \"if nil, it doesn't animate\" requirement exactly, since there is\n * nothing here to animate.\n *\n * Populated: grows to a single fixed-height row via 'max-height' (not 'height: auto', which can't\n * transition) and gains a hairline seam using the SAME border/token '.jini-composer-footer' below\n * already uses one zone down -- reads as one more zone of the same control, not a new floating\n * box. The transition runs both directions, so removing the last pinned item gets a matching exit,\n * not an instant cut.\n *\n * 'overflow-x: auto' turns this into a single scrollable row instead of the wrap-and-grow a naive\n * flex row would do with six or more items; '> *' keeps any direct host child (a single tray\n * wrapper, a bare row of buttons, anything) from shrinking to fit, so overflow -- not internal\n * wrapping -- is what kicks in once content is wider than the zone.\n *\n * The \"more content\" cue is a real shadow/vignette, not a plain alpha fade -- an alpha fade toward\n * this zone's own background was tried first and rejected on review: the chip and the zone\n * background are already close in luminance, so fading a chip's OWN pixels toward transparent read\n * as \"something is clipped/broken\", not \"swipe for more\", at a glance with no hover. The four-layer\n * 'background' below is the standard scroll-shadow technique instead: a real 'color-mix'-tinted\n * vignette (darkens in a light theme, lightens in a dark one, since it's mixed from the theme's OWN\n * text color rather than a fixed hex) drawn at each edge -- but each 'scroll'-attached shadow layer\n * is covered by a same-edge 'local'-attached solid-to-transparent layer that scrolls WITH the\n * content, so the shadow is only ever visible on the edges that still have more to reveal: at rest\n * (scrolled to the start) the left shadow is hidden and the right one shows; scroll all the way to\n * the true end and the right shadow hides too. Verified live (not just by reading the technique):\n * scrolling this element from 0 to its max scrollLeft measurably fades the right cover's computed\n * background-position across the full track and the shadow disappears exactly at the end.\n */\n.jini-chat-pane .jini-composer-leading {\n display: flex;\n align-items: center;\n overflow-x: auto;\n overflow-y: hidden;\n max-height: 0;\n padding: 0 12px;\n border-bottom: 0 solid var(--jini-chat-border-soft);\n opacity: 0;\n scrollbar-width: thin;\n transition:\n max-height .22s cubic-bezier(.3, .1, .25, 1),\n padding .22s cubic-bezier(.3, .1, .25, 1),\n border-bottom-width .22s ease,\n opacity .16s ease;\n background-color: var(--jini-chat-subtle);\n background-repeat: no-repeat;\n background-attachment: local, local, scroll, scroll;\n background-position: 0 0, 100% 0, 0 0, 100% 0;\n background-size: 26px 100%, 26px 100%, 20px 100%, 20px 100%;\n background-image:\n linear-gradient(to right, var(--jini-chat-subtle), transparent),\n linear-gradient(to left, var(--jini-chat-subtle), transparent),\n linear-gradient(to right, color-mix(in srgb, var(--jini-chat-text) 30%, transparent), transparent),\n linear-gradient(to left, color-mix(in srgb, var(--jini-chat-text) 30%, transparent), transparent);\n}\n.jini-chat-pane .jini-composer-leading:not(:empty) {\n max-height: 64px;\n padding: 10px 12px;\n border-bottom-width: 1px;\n opacity: 1;\n}\n.jini-chat-pane .jini-composer-leading > * {\n flex: none;\n min-width: max-content;\n}\n@media (prefers-reduced-motion: reduce) {\n .jini-chat-pane .jini-composer-leading { transition: none; }\n}\n.jini-chat-pane .jini-attachment-tray {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n max-height: 88px;\n overflow-y: auto;\n padding: 8px 10px 0;\n scrollbar-width: thin;\n}\n.jini-chat-pane .jini-attachment-tray:empty { display: none; }\n/*\n * The sanctioned reuse path (see '.jini-composer-leading''s own doc above and 'AttachmentTray''s\n * module doc): a host nesting Jini's own tray/chip classes inside the pinned-context zone to match\n * the real attachment tray's look. Un-does the wrap-and-grow rule directly above -- meant for that\n * tray's OWN use one zone down, where wrapping onto a second line is fine -- so the OUTER zone's\n * single scroll axis is the one that wins here instead. The real file-attachment tray is\n * unaffected: it always renders as this zone's sibling ('Composer.tsx'), never its descendant.\n */\n.jini-chat-pane .jini-composer-leading .jini-attachment-tray {\n flex-wrap: nowrap;\n max-height: none;\n overflow: visible;\n padding: 0;\n}\n/* Half the tray's width, so a sidebar-width pane fits two chips per row and its 88px cap shows two\n rows (four chips) before scrolling; at full width each chip took a row and the third was hidden. */\n.jini-chat-pane .jini-attachment-chip {\n display: inline-flex;\n align-items: center;\n box-sizing: border-box;\n min-width: 0;\n max-width: min(calc(50% - 3px), 290px);\n height: 36px;\n padding: 3px 4px 3px 5px;\n color: var(--jini-chat-text);\n background: color-mix(in srgb, var(--jini-chat-panel) 88%, var(--jini-chat-subtle));\n border: 1px solid var(--jini-chat-border);\n border-radius: 9px;\n box-shadow: 0 1px 1px rgba(13, 12, 10, .025);\n animation: jini-chat-chip-in .16s ease both;\n}\n.jini-chat-pane .jini-composer-leading .jini-attachment-chip { max-width: min(100%, 290px); }\n@keyframes jini-chat-chip-in {\n from { opacity: 0; transform: translateY(3px) scale(.94); }\n to { opacity: 1; transform: none; }\n}\n@media (prefers-reduced-motion: reduce) {\n .jini-chat-pane .jini-attachment-chip { animation: none; }\n}\n.jini-chat-pane .jini-attachment-chip-body {\n display: flex;\n align-items: center;\n min-width: 0;\n gap: 7px;\n /* A button (opens the preview modal), reset to the chip's own look. */\n padding: 0;\n font: inherit;\n color: inherit;\n text-align: left;\n background: none;\n border: 0;\n border-radius: 7px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-attachment-chip-body:focus-visible {\n outline: 2px solid var(--jini-chat-accent);\n outline-offset: 2px;\n}\n/* A staged image this browser holds: the thumbnail takes the icon tile's place, same size. */\n.jini-chat-pane .jini-attachment-chip-thumb {\n display: block;\n flex: 0 0 auto;\n width: 26px;\n height: 26px;\n object-fit: cover;\n border-radius: 7px;\n background: var(--jini-chat-subtle);\n}\n.jini-chat-pane .jini-attachment-chip-icon {\n display: grid;\n flex: 0 0 auto;\n place-items: center;\n width: 26px;\n height: 26px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border-radius: 7px;\n}\n.jini-chat-pane .jini-attachment-chip-icon.is-image {\n color: var(--jini-chat-accent);\n background: var(--jini-chat-accent-soft);\n}\n.jini-chat-pane .jini-attachment-chip-copy {\n display: flex;\n min-width: 0;\n flex-direction: column;\n line-height: 1.15;\n}\n.jini-chat-pane .jini-attachment-chip-name {\n overflow: hidden;\n font-size: 11.5px;\n font-weight: 550;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-attachment-chip-size {\n color: var(--jini-chat-muted);\n font-size: 9.5px;\n}\n.jini-chat-pane .jini-attachment-remove {\n display: grid;\n flex: 0 0 auto;\n place-items: center;\n width: 25px;\n height: 25px;\n margin-left: 4px;\n padding: 0;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 0;\n border-radius: 7px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-attachment-remove:hover,\n.jini-chat-pane .jini-attachment-remove:focus-visible {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n outline: none;\n}\n/* Phones: the 25px \"x\" gets an invisible 44px tap area; the chip keeps its size. The area ends at the\n chip's bottom edge and grows up into the tray's 8px top padding / row gap, because growing down\n past the last row would make the 88px tray scroll by a few pixels. */\n@media (max-width: 640px), (pointer: coarse) {\n .jini-chat-pane .jini-attachment-remove { position: relative; }\n .jini-chat-pane .jini-attachment-remove::before {\n content: '';\n position: absolute;\n bottom: -5.5px;\n left: 50%;\n width: 44px;\n height: 44px;\n transform: translateX(-50%);\n }\n}\n.jini-chat-pane .jini-composer-input {\n display: block;\n box-sizing: border-box;\n width: 100%;\n min-height: 92px;\n resize: none;\n padding: 12px 14px 8px;\n color: var(--jini-chat-text);\n background: transparent;\n border: 0;\n outline: 0;\n font-size: 13.5px;\n line-height: 1.6;\n}\n/* Shared focus-zoom floor: embeds at every width, all panes on small/touch viewports. */\n.jini-chat-embed .jini-chat-pane .jini-composer-input { font-size: 16px; }\n@media (max-width: 640px), (pointer: coarse) {\n .jini-chat-pane.jini-chat-pane .jini-composer-input { font-size: 16px; }\n}\n.jini-chat-pane .jini-composer-input::placeholder { color: var(--jini-chat-faint); }\n.jini-chat-pane .jini-composer-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n min-height: 42px;\n gap: 6px;\n padding: 6px 8px 8px;\n border-top: 1px solid var(--jini-chat-border-soft);\n}\n.jini-chat-pane .jini-composer-attachment-picker { display: inline-flex; }\n/* Host controls slotted at the start of the footer row, right after the attach/discovery \"+\"\n trigger above -- see ComposerSlots.footerLeadingAccessory's own doc (slots.ts) for why this\n exists as a third footer slot alongside footerAccessories/plusMenuItems. */\n.jini-chat-pane .jini-composer-footer-leading { display: inline-flex; align-items: center; }\n/* NOT 'position: relative' \u2014 the discovery popover (below) anchors off '.jini-composer' itself\n (the nearest positioned ancestor once this wrapper opts out), not off this small trigger-button\n wrapper. Anchoring to the wrapper put the popover's bottom edge at the wrapper's own top edge \u2014\n inside the footer, just below the textarea \u2014 so any popover taller than a couple of rows grew\n upward straight over the textarea and blocked clicks into it (reproduced live: Playwright's own\n click on the textarea failed with \"intercepts pointer events\" while the popover was open). */\n.jini-chat-pane .jini-composer-discovery { display: inline-flex; }\n.jini-chat-pane .jini-composer-discovery-menu,\n.jini-chat-pane .jini-composer-slash-menu {\n position: absolute;\n z-index: 8;\n display: flex;\n flex-direction: column;\n gap: 4px;\n max-height: 280px;\n overflow-y: auto;\n padding: 8px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 10px;\n box-shadow: 0 12px 30px rgb(0 0 0 / 14%);\n}\n/* Both anchor off '.jini-composer' ('bottom: calc(100% + 6px)' = fully above the textarea AND the\n footer, not just above the trigger button) so the popover floats as a clean detached card instead\n of overlapping the input it sits next to. */\n.jini-chat-pane .jini-composer-discovery-menu {\n inset-inline-start: 8px;\n bottom: calc(100% + 6px);\n width: min(280px, calc(100vw - 32px));\n}\n.jini-chat-pane .jini-composer-slash-menu {\n inset-inline: 8px;\n bottom: calc(100% + 6px);\n}\n.jini-chat-pane .jini-composer-discovery-group { display: flex; flex-direction: column; gap: 2px; }\n.jini-chat-pane .jini-composer-discovery-group-label {\n padding: 6px 8px 2px;\n color: var(--jini-chat-faint);\n font-size: 10px;\n font-weight: 700;\n letter-spacing: .08em;\n text-transform: uppercase;\n}\n.jini-chat-pane .jini-composer-discovery-item {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 2px;\n width: 100%;\n padding: 7px 8px;\n color: inherit;\n text-align: start;\n background: transparent;\n border: 0;\n border-radius: 7px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-composer-discovery-item:hover {\n background: var(--jini-chat-panel);\n}\n.jini-chat-pane .jini-composer-discovery-item.is-active {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-accent-soft);\n outline: 1px solid var(--jini-chat-accent);\n outline-offset: -1px;\n box-shadow: inset 3px 0 0 var(--jini-chat-accent);\n}\n.jini-chat-pane .jini-composer-discovery-item small { color: var(--jini-chat-muted); }\n/* The full description used to print under every row unconditionally, which is why the palette\n grew to dozens of multi-line rows on a bare \"/\" \u2014 kept in the accessibility tree at rest (never\n 'display: none', which most screen readers drop from that tree) so 'aria-describedby' keeps\n resolving whether or not anything is hovering.\n FIXED (was: opacity-only toggle, THEN an absolutely-positioned overlay) \u2014 two owner-reported\n bugs in sequence on the same element:\n 1. 'opacity: 0' alone hides a box visually but does not remove it from layout, so an\n absolutely-positioned tooltip sized to its full natural content height (routinely 50-260px\n for a real description) still counted toward its scrolling ancestor's scrollable overflow\n region while invisible ('scrollHeight' measured 501px against a 242px 'clientHeight' for a\n 6-row list), leaving a blank scrollable gap after the last real row. Fixed by collapsing the\n box itself at rest ('max-height: 0; overflow: hidden;'), not just hiding it \u2014 carried\n forward below.\n 2. That fix still left this 'position: absolute; top: 100%' of its own row (formerly anchored\n via 'position: relative' on '.jini-composer-discovery-item' above, now removed \u2014 nothing\n anchors off it anymore): 'z-index' only controls paint order, not which box a box is\n confined to, so on hover/focus the expanded tooltip painted directly over whichever row\n happened to sit below it in the SAME scrollable list (owner report: hovering \"UI/UX Design\n (Skill)\" covered the \"/mcp\" entry beneath it \u2014 not a stacking-order problem, a containment\n one). Fixed by dropping 'position: absolute' entirely: this is now a normal in-flow child of\n the row's own 'flex-direction: column' box, still collapsed to negligible size at rest via\n 'max-height: 0; overflow: hidden;', but on hover/focus it grows the ROW's own height instead\n of floating a detached box over the next row \u2014 the popover's flex column (and every row\n below) gets pushed down to make room, so it can never occlude a sibling item. This also\n retires the panel-styled \"floating card\" look (background/border/shadow): an in-flow caption\n reads correctly with the row's own muted small-text convention above, and dropping it removes\n the last reason this needed its own stacking context. 'pointer-events: none' is kept for\n parity even though nothing sits under it to protect anymore. */\n.jini-chat-pane .jini-composer-discovery-description {\n width: 100%;\n max-height: 0;\n padding: 0;\n overflow: hidden;\n color: var(--jini-chat-muted);\n font-size: 12px;\n font-weight: 400;\n line-height: 1.4;\n white-space: normal;\n opacity: 0;\n pointer-events: none;\n transition: opacity .12s ease;\n}\n.jini-chat-pane .jini-composer-discovery-item:hover .jini-composer-discovery-description,\n.jini-chat-pane .jini-composer-discovery-item:focus-visible .jini-composer-discovery-description {\n max-height: none;\n padding-top: 2px;\n overflow: visible;\n opacity: 1;\n}\n/* '<code>'/'<small>' both default to the browser's UA styling (monospace at an unrelated size)\n with nothing here overriding it \u2014 computed font-SIZE happened to already match the row label\n (13.5px), so the visible mismatch is font-FAMILY: raw monospace next to the label's sans-serif\n stack reads as a different, larger type scale even at equal size. Inheriting the row's own font\n puts both on one consistent scale. */\n.jini-chat-pane .jini-composer-slash-argument,\n.jini-chat-pane .jini-composer-slash-confirm-badge {\n font-family: inherit;\n font-size: inherit;\n font-weight: 400;\n color: var(--jini-chat-muted);\n}\n.jini-chat-pane .jini-composer-file-input {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n clip-path: inset(50%);\n}\n.jini-chat-pane .jini-composer-attach {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n padding: 0;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 1px solid transparent;\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-composer-attach:hover:not(:disabled) {\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border-color: var(--jini-chat-border);\n}\n.jini-chat-pane .jini-composer-attach:disabled { opacity: .45; cursor: default; }\n/* The working-directory trigger button reuses '.jini-composer-attach' itself (Composer.tsx) rather\n than a class duplicated here, so it is byte-identical in size/hit-area/hover/focus to its\n neighbour rather than an approximation that could drift out of sync. Only the wrapper and its\n popover are new. NOT 'position: relative' on the wrapper, for the same reason noted above\n '.jini-composer-discovery': the popover's un-measured first paint (before the layout effect sets\n its 'position: fixed' inline style) falls back to this rule's 'position: absolute', which needs\n '.jini-composer' itself \u2014 the nearest positioned ancestor \u2014 not this wrapper, to anchor against. */\n.jini-chat-pane .jini-composer-workdir { display: inline-flex; }\n.jini-chat-pane .jini-composer-workdir-panel {\n position: absolute;\n z-index: 8;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px;\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 10px;\n box-shadow: 0 12px 30px rgb(0 0 0 / 14%);\n}\n.jini-chat-pane .jini-composer-workdir-input {\n flex: 1;\n min-width: 160px;\n padding: 6px 8px;\n color: var(--jini-chat-text);\n font: inherit;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 6px;\n}\n.jini-chat-pane .jini-composer-workdir-confirm {\n padding: 6px 10px;\n color: white;\n white-space: nowrap;\n background: var(--jini-chat-text-strong);\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n/* `.jini-icon-spin` is `<Icon name=\"spinner\">` (the tool-row \"Running\" badge). Its keyframes used to\n live only in reference.css, which no host imports, so the badge sat still. */\n.jini-composer-spinner,\n.jini-runtime-spinner,\n.jini-icon-spin { animation: jini-chat-spin .8s linear infinite; }\n@keyframes jini-chat-spin { to { transform: rotate(360deg); } }\n.jini-chat-pane .jini-composer-footer-accessories {\n display: flex;\n margin-left: auto;\n min-width: 0;\n}\n.jini-chat-pane .jini-composer-send-group {\n display: flex;\n flex-shrink: 0;\n gap: 6px;\n}\n/* A footer with no attach/accessory controls before the send group (a public visitor chat) would\n leave the lone send button at the start under the footer's space-between; keep it at the end. */\n.jini-chat-pane .jini-composer-send-group:first-child { margin-left: auto; }\n.jini-chat-pane .jini-composer-send {\n display: grid;\n place-items: center;\n width: 32px;\n height: 32px;\n padding: 0;\n color: white;\n background: var(--jini-chat-text-strong);\n border: 0;\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-composer-send:disabled {\n color: #aaa59e;\n background: #ece8e2;\n cursor: default;\n}\n.jini-chat-pane__workdir {\n display: flex;\n align-items: center;\n gap: 7px;\n min-width: 0;\n padding: 5px 4px 0;\n color: var(--jini-chat-muted);\n font-size: 13px;\n}\n.jini-chat-pane__workdir code {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-picker {\n position: relative;\n display: inline-flex;\n align-items: flex-start;\n}\n.jini-chat-pane .jini-working-dir-trigger-row {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n}\n.jini-chat-pane .jini-working-dir-trigger {\n appearance: none;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 6px;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 0;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-working-dir-trigger:hover,\n.jini-chat-pane .jini-working-dir-trigger:focus-visible {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n outline: none;\n}\n.jini-chat-pane .jini-working-dir-trigger.invalid { color: var(--danger); }\n.jini-chat-pane .jini-working-dir-trigger-icon { flex: 0 0 auto; opacity: .75; }\n.jini-chat-pane .jini-working-dir-trigger.invalid .jini-working-dir-trigger-icon {\n color: var(--danger);\n opacity: 1;\n}\n.jini-chat-pane .jini-working-dir-trigger-label {\n max-width: 240px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-trigger-chevron { flex: 0 0 auto; opacity: .55; }\n.jini-chat-pane .jini-working-dir-panel,\n.jini-chat-pane .jini-working-dir-flyout {\n position: absolute;\n z-index: 90;\n display: flex;\n flex-direction: column;\n min-width: 210px;\n padding: 5px;\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border-strong);\n border-radius: 12px;\n box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 10px 28px -10px rgba(0, 0, 0, .18);\n}\n.jini-chat-pane .jini-working-dir-panel {\n top: calc(100% + 6px);\n left: 0;\n}\n.jini-chat-pane .jini-working-dir-panel.up {\n top: auto;\n bottom: calc(100% + 6px);\n}\n.jini-chat-pane .jini-working-dir-item {\n appearance: none;\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n min-height: 32px;\n padding: 0 9px;\n color: var(--jini-chat-text-strong);\n background: transparent;\n border: 0;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n text-align: left;\n cursor: pointer;\n}\n.jini-chat-pane .jini-working-dir-item:hover,\n.jini-chat-pane .jini-working-dir-item:focus-visible,\n.jini-chat-pane .jini-working-dir-recent-item:hover,\n.jini-chat-pane .jini-working-dir-recent-item:focus-visible {\n background: var(--jini-chat-subtle);\n outline: none;\n}\n.jini-chat-pane .jini-working-dir-item-icon { flex: 0 0 auto; opacity: .8; }\n.jini-chat-pane .jini-working-dir-item > span {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-item-chevron { margin-left: auto; opacity: .55; }\n.jini-chat-pane .jini-working-dir-submenu-row { position: relative; }\n.jini-chat-pane .jini-working-dir-flyout {\n top: -5px;\n left: 100%;\n z-index: 91;\n min-width: 220px;\n max-width: 320px;\n margin-left: 4px;\n}\n.jini-chat-pane .jini-working-dir-flyout.up { top: auto; bottom: -5px; }\n.jini-chat-pane .jini-working-dir-flyout::before {\n position: absolute;\n top: 0;\n left: -8px;\n width: 8px;\n height: 100%;\n content: \"\";\n}\n.jini-chat-pane .jini-working-dir-recent-item {\n appearance: none;\n display: grid;\n grid-template-rows: auto auto;\n grid-template-columns: auto 1fr;\n align-items: center;\n width: 100%;\n min-height: 36px;\n padding: 4px 9px;\n color: var(--jini-chat-text-strong);\n background: transparent;\n border: 0;\n border-radius: 8px;\n font: inherit;\n text-align: left;\n cursor: pointer;\n column-gap: 9px;\n}\n.jini-chat-pane .jini-working-dir-recent-item .jini-working-dir-item-icon {\n grid-row: 1 / span 2;\n}\n.jini-chat-pane .jini-working-dir-recent-name,\n.jini-chat-pane .jini-working-dir-recent-path {\n grid-column: 2;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-recent-name { font-size: 13px; }\n.jini-chat-pane .jini-working-dir-recent-path {\n color: var(--jini-chat-muted);\n font-size: 11px;\n opacity: .7;\n}\n.jini-chat-pane .jini-working-dir-empty {\n padding: 8px 10px;\n color: var(--jini-chat-muted);\n font-size: 12px;\n white-space: nowrap;\n opacity: .7;\n}\n.jini-runtime-picker { position: relative; }\n.jini-runtime-trigger {\n display: inline-grid;\n grid-template-columns: 24px 14px;\n align-items: center;\n gap: 7px;\n height: 32px;\n min-width: 0;\n padding: 3px 7px 3px 5px;\n color: var(--jini-chat-text);\n background: transparent;\n border: 0;\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-runtime-trigger:hover { background: var(--jini-chat-subtle); }\n.jini-runtime-trigger__copy { display: none; }\n.jini-runtime-trigger__copy strong {\n overflow: hidden;\n max-width: 122px;\n font-size: 11px;\n font-weight: 650;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-runtime-trigger__copy small {\n overflow: hidden;\n max-width: 122px;\n color: var(--jini-chat-muted);\n font-size: 9px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-runtime-trigger__chevron { color: var(--jini-chat-muted); font-size: 12px; }\n.jini-runtime-agent-icon.agent-icon { flex: 0 0 auto; object-fit: contain; }\n.jini-runtime-agent-icon.agent-icon-mono {\n display: inline-block;\n background-color: currentColor;\n -webkit-mask-repeat: no-repeat;\n mask-repeat: no-repeat;\n -webkit-mask-position: center;\n mask-position: center;\n -webkit-mask-size: contain;\n mask-size: contain;\n}\n.jini-runtime-agent-icon.agent-icon-fallback {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #74716b;\n background: #f4f5f7;\n border: 1px solid #e1e5eb;\n border-radius: 6px;\n font-weight: 700;\n}\n.jini-runtime-popover {\n --jini-chat-panel: #fdfcfa;\n --jini-chat-text: #1a1916;\n --jini-chat-text-strong: #0d0c0a;\n --jini-chat-muted: #74716b;\n --jini-chat-faint: #b3b0a8;\n --jini-chat-border: #e1e5eb;\n --jini-chat-border-soft: #edf0f4;\n --jini-chat-subtle: #f4f5f7;\n box-sizing: border-box;\n padding: 8px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Microsoft YaHei UI\", \"Noto Sans\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n font-size: 13.5px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n}\n.jini-runtime-popover__head {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 10px 12px 12px;\n border-bottom: 1px solid var(--jini-chat-border-soft);\n}\n.jini-runtime-popover__head strong { color: var(--jini-chat-text-strong); font-size: 14px; font-weight: 650; }\n.jini-runtime-popover__head span { color: var(--jini-chat-muted); font-size: 12px; }\n.jini-runtime-mode,\n.jini-runtime-agent,\n.jini-runtime-rescan {\n display: flex;\n align-items: center;\n gap: 11px;\n box-sizing: border-box;\n width: 100%;\n min-height: 40px;\n padding: 8px 11px;\n color: var(--jini-chat-text);\n background: transparent;\n border: 0;\n border-radius: 8px;\n text-align: left;\n cursor: pointer;\n}\n.jini-runtime-mode:hover,\n.jini-runtime-agent:hover:not(:disabled),\n.jini-runtime-rescan:hover:not(:disabled) { background: var(--jini-chat-subtle); }\n.jini-runtime-mode.is-active { font-weight: 650; }\n.jini-runtime-mode:disabled,\n.jini-runtime-agent:disabled,\n.jini-runtime-rescan:disabled { color: var(--jini-chat-faint); cursor: default; }\n.jini-runtime-mode__meta,\n.jini-runtime-agent__status {\n margin-left: auto;\n color: var(--jini-chat-muted);\n font-size: 12px;\n white-space: nowrap;\n}\n.jini-runtime-check { flex: 0 0 auto; color: var(--jini-chat-text); }\n/* Matches .jini-message-attachment-chip/.jini-md-table-expand's pill chrome (same\n background/border/radius formula) so this reads as the same badge language already used\n elsewhere in the pane, not a new one. Sits between the agent name and its status column \u2014\n flex: 0 0 auto keeps it from stretching or shrinking when the row is tight. */\n.jini-runtime-agent__badge {\n flex: 0 0 auto;\n padding: 2px 8px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n font-size: 10px;\n font-weight: 500;\n white-space: nowrap;\n}\n.jini-runtime-section-label {\n padding: 12px 11px 5px;\n color: var(--jini-chat-faint);\n font-size: 11px;\n font-weight: 650;\n letter-spacing: .08em;\n text-transform: uppercase;\n}\n.jini-runtime-agent.is-active { background: transparent; }\n.jini-runtime-agent__copy {\n display: flex;\n flex: 1;\n flex-direction: column;\n min-width: 0;\n}\n.jini-runtime-agent__copy strong { font-size: 13.5px; font-weight: 500; }\n.jini-runtime-agent__copy small {\n overflow: hidden;\n color: #8b857e;\n font-size: 10px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-runtime-empty { padding: 10px 11px; color: var(--jini-chat-muted); font-size: 12px; }\n.jini-runtime-models {\n display: flex;\n flex-direction: column;\n gap: 9px;\n margin-top: 5px;\n padding: 10px 11px 8px;\n border-top: 1px dashed var(--jini-chat-border);\n}\n.jini-runtime-select {\n display: flex;\n flex-direction: column;\n gap: 5px;\n color: var(--jini-chat-muted);\n font-size: 13px;\n}\n.jini-runtime-select select {\n box-sizing: border-box;\n width: 100%;\n min-height: 40px;\n padding: 7px 10px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n}\n/*\n * The BYOK model, rendered as a value rather than a control \u2014 see RuntimeByokDetails for why it\n * is not a disabled select. It occupies the same box the sibling selects do so the row still\n * reads as part of the same list, but carries no border: a bordered box with no affordance is\n * exactly the \"looks editable, isn't\" shape the plain text exists to avoid.\n */\n/*\n * The BYOK model list, which CustomSelect portals to document.body. runtimePopoverPosition puts\n * the runtime popover at z-index 1000; the shared .jini-select-menu default is 60, which is right\n * everywhere else and puts this menu behind the very popover that opened it. One above the\n * popover, not far above: it must clear its own surface and nothing else.\n */\n.jini-select-menu.jini-runtime-model-menu {\n z-index: 1001;\n}\n.jini-runtime-byok-model strong {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n min-height: 40px;\n padding: 7px 10px;\n color: var(--jini-chat-text);\n font-weight: 600;\n word-break: break-all;\n}\n/*\n * Tool-call cards (ToolCard.tsx) and the per-turn usage summary (MessageRow.tsx) ship no CSS of\n * their own \u2014 see this file's own header note on why the package supplies a default theme at all.\n * Collapsed by default (.op-card-head is the only always-visible part), so a run with a dozen tool\n * calls reads as a dozen one-line rows, not a page of raw JSON.\n */\n.jini-chat-pane .jini-message-tools {\n display: flex;\n flex-direction: column;\n gap: 6px;\n width: 100%;\n}\n.jini-chat-pane .op-card {\n border: 1px solid var(--jini-chat-border);\n border-radius: var(--radius);\n background: var(--jini-chat-subtle);\n overflow: hidden;\n font-size: 13px;\n}\n.jini-chat-pane .op-card-head {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 7px 10px;\n background: none;\n border: 0;\n font: inherit;\n text-align: left;\n cursor: pointer;\n color: inherit;\n}\n.jini-chat-pane .op-status { display: inline-flex; flex-shrink: 0; }\n.jini-chat-pane .op-status-ok { color: #4e875f; }\n.jini-chat-pane .op-status-error { color: var(--danger); }\n.jini-chat-pane .op-status-running { color: var(--jini-chat-accent); }\n.jini-chat-pane .op-title { font-weight: 600; flex-shrink: 0; }\n.jini-chat-pane .shimmer-text { animation: jini-chat-shimmer 1.6s ease-in-out infinite; }\n@keyframes jini-chat-shimmer { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }\n@media (prefers-reduced-motion: reduce) { .jini-chat-pane .shimmer-text { animation: none; opacity: .6; } }\n.jini-chat-pane .op-meta {\n color: var(--jini-chat-muted);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n min-width: 0;\n}\n.jini-chat-pane .op-expand-chev { flex-shrink: 0; color: var(--jini-chat-faint); }\n/* Collapsed by default: 0 max-height clips the inner content, no display:none \u2014 keeps the height\n transition instead of an instant jump cut. */\n.jini-chat-pane .accordion-collapsible { max-height: 0; overflow: hidden; transition: max-height .15s ease; }\n.jini-chat-pane .accordion-collapsible.open { max-height: 480px; overflow-y: auto; }\n.jini-chat-pane .accordion-collapsible-inner { padding: 0 10px 10px; }\n.jini-chat-pane .op-card-detail { display: flex; flex-direction: column; gap: 6px; }\n.jini-chat-pane .op-path,\n.jini-chat-pane .op-command,\n.jini-chat-pane .op-output {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 6px;\n padding: 6px 8px;\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n}\n/* The \"Waiting for the result or answer\u2026\" / \"Finished with no output.\" line in an expanded tool row (ToolCard.tsx's ToolCallDetail). */\n.jini-chat-pane .op-detail-note { margin: 0; font-size: 12px; color: var(--jini-chat-muted); }\n.jini-chat-pane .op-open {\n border: 1px solid var(--jini-chat-border-strong);\n border-radius: 6px;\n background: var(--jini-chat-panel);\n padding: 2px 8px;\n font-size: 11px;\n cursor: pointer;\n}\n/* The \"Done \u00B7 6m 29s \u00B7 2612 out \u00B7 $0.4028\" line \u2014 real numbers from the run's own kind:'usage' event. */\n.jini-chat-pane .jini-message-usage {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n color: var(--jini-chat-muted);\n margin-top: 2px;\n}\n.jini-chat-pane .jini-message-usage-dot { font-size: 8px; color: #4e875f; }\n/* toolCallDisplay 'collapsed' (ToolCallGroup.tsx): one quiet line per turn, cards open under it. */\n.jini-chat-pane .jini-tool-group { display: flex; flex-direction: column; gap: 6px; width: 100%; }\n.jini-chat-pane .jini-tool-group__toggle {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n align-self: flex-start;\n max-width: 100%;\n padding: 2px 0;\n background: none;\n border: 0;\n font: inherit;\n font-size: 12px;\n color: var(--jini-chat-muted);\n cursor: pointer;\n}\n.jini-chat-pane .jini-tool-group__toggle:hover { color: inherit; }\n.jini-chat-pane .jini-tool-group__toggle:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 2px; border-radius: 4px; }\n.jini-chat-pane .jini-tool-group__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.jini-chat-pane .jini-tool-group[data-failed=\"true\"] .jini-tool-group__toggle { color: var(--danger); }\n/* showThinking (MessageRow.tsx's ThinkingBlock): the model's reasoning, folded, set apart from the answer. */\n.jini-chat-pane .jini-message-thinking { font-size: 12px; color: var(--jini-chat-muted); }\n.jini-chat-pane .jini-message-thinking > summary { cursor: pointer; width: fit-content; }\n.jini-chat-pane .jini-message-thinking__text {\n margin-top: 4px;\n padding-left: 10px;\n border-left: 2px solid var(--jini-chat-border);\n white-space: pre-wrap;\n word-break: break-word;\n}\n/*\n * Fenced code block ('Markdown.tsx''s 'renderBlock' case 'code') and inline 'code' span visual\n * treatment \u2014 previously entirely unstyled (bare UA-default '<pre><code>', no background, border,\n * or padding; a downstream admin host's own stylesheet already flagged this gap in a note next to\n * its unrelated horizontal-scroll fix for the same element). Lives HERE, in the package's own\n * injected default theme, rather than in a host's stylesheet: a host CAN already remap the exact\n * '--jini-chat-*' custom properties this rule reads (one such host's '.admin-chat-dock .jini-chat-\n * pane' block already does, for every other rule in this file), so putting the fix here makes every\n * Jini host look correct by default AND lets a host match its own palette for free, with no new\n * host-side override rule needed at all \u2014 the reverse (host-only CSS) would leave every other\n * embedder of '@jini-ai/chat' with the same unstyled '<pre>' this fix exists to correct.\n * 'max-width'/'overflow-x' here duplicate (harmlessly) a narrower fix that same host's own\n * '.jini-message-content pre' rule already carries for its own fixed-width dock specifically \u2014 this\n * package cannot assume every host has that rule, so a real host-agnostic default belongs here too.\n * NOTE: this comment avoids backtick quoting on purpose \u2014 this whole stylesheet lives inside a JS\n * template literal, so a literal backtick here would terminate the string, not just this comment.\n */\n.jini-chat-pane .jini-message-content pre {\n margin: 6px 0;\n max-width: 100%;\n overflow-x: auto;\n padding: 10px 12px;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12.5px;\n line-height: 1.5;\n}\n.jini-chat-pane .jini-message-content pre code {\n padding: 0;\n background: none;\n border: 0;\n color: inherit;\n font: inherit;\n}\n.jini-chat-pane .jini-message-content :not(pre) > code {\n padding: .15em .4em;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 4px;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: .9em;\n}\n/*\n * Copy affordance for a fenced code block ('Markdown.tsx''s 'CodeBlock'/'CopyCodeButton'). The\n * button lives in its own toolbar row fused to the TOP of the '<pre>' below, not overlaid on top\n * of the code itself \u2014 an overlay would sit over whatever text happens to render at that corner,\n * and '<pre>' already carries its own horizontal scrollbar along its own bottom edge, so a row\n * above it can never collide with that scrollbar either. '.jini-md-code-block' takes over the\n * '<pre>' rule's own top/bottom margin above so the two don't stack, and the toolbar/pre pair\n * share one rounded box (toolbar keeps the top corners, '<pre>' below loses them) instead of two\n * separately-bordered boxes stacked on each other.\n */\n.jini-chat-pane .jini-message-content .jini-md-code-block {\n margin: 6px 0;\n}\n.jini-chat-pane .jini-message-content .jini-md-code-block pre {\n margin: 0;\n border-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.jini-chat-pane .jini-md-code-toolbar {\n display: flex;\n justify-content: flex-end;\n padding: 3px;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-bottom: 0;\n border-radius: 8px 8px 0 0;\n}\n.jini-chat-pane .jini-md-code-copy {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n padding: 0;\n color: var(--jini-chat-faint);\n background: none;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-md-code-copy:hover { color: var(--jini-chat-muted); background: var(--jini-chat-border); }\n.jini-chat-pane .jini-md-code-copy:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 1px; }\n/* Visually hidden but present in the DOM (never display:none) so this polite live region still\n gets announced \u2014 same clip-rect technique 'jini-message-copy-status' above already uses. */\n.jini-chat-pane .jini-md-code-copy-status {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n}\n/*\n * GFM pipe tables ('Markdown.tsx''s 'TableBlock') \u2014 same reasoning as the code-block rules directly\n * above for living in the package's own default theme rather than a host stylesheet. '.jini-md-\n * table-wrap' gets its own horizontal scrollbar so a wide table scrolls internally instead of\n * dragging '.jini-message-list' sideways with it (the exact failure mode that same downstream host's\n * stylesheet documents for the code-block case \u2014 the same fix shape applies here). The \"Expand\n * table\" button only renders ('TableBlock''s own logic) once the table has genuinely overflowed\n * that wrap.\n */\n.jini-md-table-wrap {\n max-width: 100%;\n overflow-x: auto;\n}\n.jini-md-table {\n border-collapse: collapse;\n font-size: 12.5px;\n}\n/*\n * 'min-width' here, not on '.jini-md-table' itself, and picked per column rather than as one flat\n * table-wide number -- a flat table minimum sized for a typical 2-column table would leave a\n * 5-column table crushed just as badly as before (same failure this comment's neighbor above\n * describes), while one sized for 5 columns would force pointless overflow on a simple 2-column\n * table. Per-cell 'min-width' scales the effective table floor with the real column count instead.\n * 130px was picked empirically, not guessed: measured (real Chromium, not jsdom -- 'scrollWidth'/\n * 'clientWidth' are unimplemented there) against this package's narrowest known real host, an\n * admin chat dock fixed at 380px wide ('admin-chat-dock' in that host's own stylesheet), which\n * nets out to roughly 238px of usable width once that dock's own message-list padding, the 80%\n * message-bubble cap, and this content's own padding are all applied. Below 120px per column the\n * table still fits inside those 238px and never overflows -- the exact bug this fix exists to\n * correct. 130px was chosen just above that measured 120px crossover (a small margin so it clears\n * reliably rather than sitting exactly on the boundary) and reads as a genuinely defensible per-\n * column floor on its own terms too: with this rule's own 6px/10px cell padding subtracted, it\n * leaves about 110px of text room, roughly 14-16 characters per wrapped line at this rule's 12.5px\n * font -- enough that a table cell wraps in short phrases instead of one word per line, which is\n * the readability floor this fix is actually chasing. One real consequence, verified rather than\n * hand-waved: at this width, ANY 2-column table in that narrow dock now overflows and gets the\n * \"Expand table\" affordance, including ones with short cell content -- there is no per-cell\n * min-width value that can overflow a long-prose 2-column table without also overflowing a short\n * one at the same column count, because the browser's own auto-layout table-width algorithm sizes\n * the table to the greater of its container's width and the sum of each column's own minimum --\n * content length past that minimum only changes how much a column wraps internally, never whether\n * the table exceeds its wrap. Accepted here: in a surface already this narrow, showing \"Expand\n * table\" a little more eagerly is the better failure mode than silently crushing a table's own\n * content illegibly, which is the state this whole rule exists to fix.\n */\n.jini-md-table th,\n.jini-md-table td {\n padding: 6px 10px;\n min-width: 130px;\n border: 1px solid var(--jini-chat-border);\n text-align: left;\n}\n.jini-md-table th {\n background: var(--jini-chat-subtle);\n color: var(--jini-chat-text-strong);\n font-weight: 650;\n}\n.jini-md-table-expand {\n margin-top: 4px;\n padding: 4px 10px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n font-size: 11px;\n cursor: pointer;\n}\n.jini-md-table-expand:hover { color: var(--jini-chat-text-strong); }\n.jini-md-table-modal {\n max-width: min(90vw, 900px);\n max-height: 80vh;\n overflow: auto;\n padding: 20px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06);\n}\n.jini-md-table-modal::backdrop {\n background: rgba(0, 0, 0, .4);\n}\n.jini-md-table-modal-close {\n position: absolute;\n top: 10px;\n right: 10px;\n width: 28px;\n height: 28px;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 0;\n border-radius: 6px;\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n}\n.jini-md-table-modal-close:hover {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n}\n@media (max-width: 560px) {\n .jini-chat-pane__header { padding-inline: 16px; }\n .jini-chat-pane .jini-message-list { padding-inline: 16px; }\n .jini-chat-pane__controls { padding-inline: 10px; }\n}\n/*\n * Attachment preview modal (MessageRow.tsx's chip -> AttachmentPreviewModal.tsx). Same\n * native-<dialog> shape as .jini-md-table-modal above (own box sized to content, not viewport;\n * ::backdrop dims the page), kept as its own class rather than reusing that one because the two\n * differ in intent: a table pop-out is a fixed-chrome expansion of content already on screen, this\n * is a general image/text/metadata viewer with its own header and a dedicated close control.\n */\n.jini-attachment-preview-dialog {\n display: flex;\n flex-direction: column;\n width: min(90vw, 720px);\n max-height: 80vh;\n padding: 0;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06);\n}\n.jini-attachment-preview-dialog::backdrop {\n background: rgba(0, 0, 0, .4);\n}\n.jini-attachment-preview-header {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--jini-chat-border-soft);\n}\n.jini-attachment-preview-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 650;\n color: var(--jini-chat-text-strong);\n}\n.jini-attachment-preview-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n width: 26px;\n height: 26px;\n margin-left: auto;\n padding: 0;\n color: var(--jini-chat-muted);\n background: none;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.jini-attachment-preview-close:hover {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n}\n.jini-attachment-preview-body {\n display: flex;\n flex: 1;\n min-height: 0;\n align-items: center;\n justify-content: center;\n padding: 16px;\n overflow: auto;\n}\n.jini-attachment-preview-image {\n display: block;\n max-width: 100%;\n max-height: calc(80vh - 90px);\n object-fit: contain;\n margin: auto;\n}\n.jini-attachment-preview-text {\n width: 100%;\n max-height: calc(80vh - 90px);\n margin: 0;\n padding: 12px;\n overflow: auto;\n color: var(--jini-chat-text);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border-soft);\n border-radius: 8px;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.jini-attachment-preview-truncated {\n width: 100%;\n margin-top: 8px;\n color: var(--jini-chat-muted);\n font-size: 11px;\n text-align: center;\n}\n.jini-attachment-preview-loading {\n color: var(--jini-chat-muted);\n font-size: 13px;\n font-style: italic;\n}\n.jini-attachment-preview-meta {\n display: flex;\n flex-direction: column;\n gap: 6px;\n width: 100%;\n max-width: 360px;\n color: var(--jini-chat-text);\n font-size: 13px;\n}\n.jini-attachment-preview-meta-row {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n}\n.jini-attachment-preview-meta-label {\n color: var(--jini-chat-faint);\n}\n.jini-attachment-preview-meta-note {\n margin-top: 6px;\n color: var(--jini-chat-muted);\n font-size: 12px;\n}\n/* Previous/next across a message's (or the composer's) images: a footer under the image, so the\n * buttons sit on the panel colour - readable over any picture - side by side in the bottom-right. */\n/* A fixed stage while stepping: sized to each image, the dialog would resize and move the buttons\n * out from under the pointer between clicks. */\n.jini-attachment-preview-dialog.has-nav .jini-attachment-preview-body {\n flex: 0 0 auto;\n height: min(calc(80vh - 152px), 480px);\n}\n.jini-attachment-preview-dialog.has-nav .jini-attachment-preview-image,\n.jini-attachment-preview-dialog.has-nav .jini-attachment-preview-text {\n max-height: 100%;\n}\n.jini-attachment-preview-nav {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 10px 14px;\n border-top: 1px solid var(--jini-chat-border-soft);\n}\n.jini-attachment-preview-count {\n margin-right: auto;\n color: var(--jini-chat-muted);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n}\n.jini-attachment-preview-step {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n padding: 0;\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-attachment-preview-step:hover {\n background: var(--jini-chat-subtle);\n border-color: var(--jini-chat-border-strong);\n}\n.jini-attachment-preview-step:focus-visible {\n outline: 2px solid var(--jini-chat-accent);\n outline-offset: 2px;\n}\n/* Phones: 44px targets. The 26px close \"x\" keeps its size inside an invisible 44px area (centred,\n so it stays within the header's 12px/14px padding); the bordered previous/next buttons just grow. */\n@media (max-width: 640px), (pointer: coarse) {\n .jini-attachment-preview-close { position: relative; }\n .jini-attachment-preview-close::before {\n content: '';\n position: absolute;\n top: 50%;\n left: 50%;\n width: 44px;\n height: 44px;\n transform: translate(-50%, -50%);\n }\n .jini-attachment-preview-step {\n width: 44px;\n height: 44px;\n }\n}\n";
|
|
2
2
|
//# sourceMappingURL=styles.d.ts.map
|
|
@@ -1790,5 +1790,23 @@ export const CHAT_PANE_STYLES = `
|
|
|
1790
1790
|
outline: 2px solid var(--jini-chat-accent);
|
|
1791
1791
|
outline-offset: 2px;
|
|
1792
1792
|
}
|
|
1793
|
+
/* Phones: 44px targets. The 26px close "x" keeps its size inside an invisible 44px area (centred,
|
|
1794
|
+
so it stays within the header's 12px/14px padding); the bordered previous/next buttons just grow. */
|
|
1795
|
+
@media (max-width: 640px), (pointer: coarse) {
|
|
1796
|
+
.jini-attachment-preview-close { position: relative; }
|
|
1797
|
+
.jini-attachment-preview-close::before {
|
|
1798
|
+
content: '';
|
|
1799
|
+
position: absolute;
|
|
1800
|
+
top: 50%;
|
|
1801
|
+
left: 50%;
|
|
1802
|
+
width: 44px;
|
|
1803
|
+
height: 44px;
|
|
1804
|
+
transform: translate(-50%, -50%);
|
|
1805
|
+
}
|
|
1806
|
+
.jini-attachment-preview-step {
|
|
1807
|
+
width: 44px;
|
|
1808
|
+
height: 44px;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1793
1811
|
`;
|
|
1794
1812
|
//# sourceMappingURL=styles.js.map
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { DurableRecovery, RecoveryPorts } from "../../core/durable-runs/ports.js";
|
|
2
2
|
export { RECOVERY_LIMIT, RECOVERY_WINDOW_MS, RUN_STALENESS_MS, SAVED_WORK_NOTICE, EXHAUSTED_NOTICE } from "../../core/durable-runs/policies.js";
|
|
3
|
-
/** Recovery coordinator behind explicit host ports. One CAS owns both the attempt and its conversation slot.
|
|
4
|
-
|
|
3
|
+
/** Recovery coordinator behind explicit host ports. One CAS owns both the attempt and its conversation slot.
|
|
4
|
+
* Each recovery decision logs one `logger.info` line (default console) of run metadata. */
|
|
5
|
+
export declare function createDurableRecovery(ports: RecoveryPorts, { logger }?: {
|
|
6
|
+
logger?: Pick<Console, "info">;
|
|
7
|
+
}): DurableRecovery;
|
|
5
8
|
//# sourceMappingURL=recover.d.ts.map
|
|
@@ -22,7 +22,22 @@ function attemptDiagnostics({ run }, _optional = {}) {
|
|
|
22
22
|
break;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
return events.slice(marker >= 0 ? marker + 1 : run.attemptBase.length).flatMap((event) =>
|
|
25
|
+
return events.slice(marker >= 0 ? marker + 1 : run.attemptBase.length).flatMap((event) => {
|
|
26
|
+
if (event.kind === "status")
|
|
27
|
+
return [event.label, event.detail ?? ""];
|
|
28
|
+
if (event.kind !== "raw")
|
|
29
|
+
return [];
|
|
30
|
+
try {
|
|
31
|
+
const result = JSON.parse(event.line);
|
|
32
|
+
// Successful CLI summaries contain usage metadata and answer prose, not failure evidence.
|
|
33
|
+
if (result?.type === "result" && result.is_error === false)
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Non-JSON stdout can still contain the executor's failure reason.
|
|
38
|
+
}
|
|
39
|
+
return [event.line];
|
|
40
|
+
}).join("\n");
|
|
26
41
|
}
|
|
27
42
|
function attemptFailure(run) {
|
|
28
43
|
const diagnostics = attemptDiagnostics({ run }, {});
|
|
@@ -40,7 +55,7 @@ function attemptFailure(run) {
|
|
|
40
55
|
const requestStatuses = diagnostics.matchAll(/\b(?:http(?:[ /]?\d(?:\.\d)?)?|(?:status|error|response)(?:[ _-]?code)?|code(?=\s*[:=#]))[\s:=#-]*(4\d\d)\b/gi);
|
|
41
56
|
if ([...requestStatuses].some((match) => match[1] !== "408" && match[1] !== "429"))
|
|
42
57
|
return "permanent";
|
|
43
|
-
if (/another answer in this chat is still running|invalid[_ -]?request|context[_ -]
|
|
58
|
+
if (/another answer in this chat is still running|invalid[_ -]?request|context[_ -](length|window)|maximum context|model.*(?:not[_ -]found|does not exist|not supported)|unsupported|does not support|permission denied|forbidden|insufficient[_ -]?(quota|balance|credit|funds)|credit balance is too low|exceeded your current quota|quota[_ -]?exceeded|usage limit|billing|payment required/i.test(diagnostics))
|
|
44
59
|
return "permanent";
|
|
45
60
|
return "retryable";
|
|
46
61
|
}
|
|
@@ -60,8 +75,9 @@ function budgetNotice(run, now) {
|
|
|
60
75
|
return EXHAUSTED_NOTICE;
|
|
61
76
|
return null;
|
|
62
77
|
}
|
|
63
|
-
/** Recovery coordinator behind explicit host ports. One CAS owns both the attempt and its conversation slot.
|
|
64
|
-
|
|
78
|
+
/** Recovery coordinator behind explicit host ports. One CAS owns both the attempt and its conversation slot.
|
|
79
|
+
* Each recovery decision logs one `logger.info` line (default console) of run metadata. */
|
|
80
|
+
export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
65
81
|
async function finalize(run, notice, canceled = false) {
|
|
66
82
|
await ports.cancelAttempt(run, {}).catch(() => undefined);
|
|
67
83
|
// Persist the notice as an event: hook errors are live-only and would leave a reloaded
|
|
@@ -108,12 +124,12 @@ export function createDurableRecovery(ports, _optional = {}) {
|
|
|
108
124
|
ports.attach(next, {});
|
|
109
125
|
return "continued";
|
|
110
126
|
}
|
|
111
|
-
async function decide(run, trigger, liveRunId) {
|
|
127
|
+
async function decide(run, trigger, liveRunId, seen) {
|
|
112
128
|
if (exclusion(run))
|
|
113
129
|
return finalize(run, "Stopped. Saved work is above.", true);
|
|
114
130
|
// A terminal executor end confirms this attempt is dead even when the daemon retains its
|
|
115
131
|
// replayable record (HTTP 200). Other triggers must obtain fresh liveness evidence.
|
|
116
|
-
const probe = trigger === "attempt-failed" ? "dead" : liveRunId === run.runId ? "live" : await ports.probe(run, {});
|
|
132
|
+
const probe = seen.probe = trigger === "attempt-failed" ? "dead" : liveRunId === run.runId ? "live" : await ports.probe(run, {});
|
|
117
133
|
if (probe === "live") {
|
|
118
134
|
await ports.store.recoveryClock({ runId: run.runId, now: ports.now(), active: false }, {});
|
|
119
135
|
ports.attach(run, {});
|
|
@@ -122,7 +138,7 @@ export function createDurableRecovery(ports, _optional = {}) {
|
|
|
122
138
|
// Diagnostics decide only the fate of an attempt that is no longer running. Read before the
|
|
123
139
|
// probe, raw stdout quoting a tool description ("OAuth-authenticated") canceled a live turn
|
|
124
140
|
// as "Not logged in" (luvira, 2026-10-08).
|
|
125
|
-
const failure = attemptFailure(run);
|
|
141
|
+
const failure = seen.failure = attemptFailure(run);
|
|
126
142
|
if (failure === "auth-failure" || failure === "inactivity-watchdog")
|
|
127
143
|
return finalize(run, failure === "auth-failure" ? "Not logged in. Saved work is above." : "Stopped. Saved work is above.", true);
|
|
128
144
|
// A bridge failure shows its own reason (and stderr): the generic notice hides what needs repair.
|
|
@@ -156,7 +172,13 @@ export function createDurableRecovery(ports, _optional = {}) {
|
|
|
156
172
|
return "terminal";
|
|
157
173
|
if (expectedRunId && expectedRunId !== run.runId)
|
|
158
174
|
return "superseded";
|
|
159
|
-
|
|
175
|
+
const seen = {};
|
|
176
|
+
const decision = await decide(run, trigger, options.liveRunId, seen);
|
|
177
|
+
// The watcher re-confirms its own live run every resolution window; log only real recovery
|
|
178
|
+
// decisions. Metadata only: diagnostics, answers and cancel reasons can hold secrets.
|
|
179
|
+
if (options.liveRunId !== run.runId)
|
|
180
|
+
logger.info("[durable-recovery] " + JSON.stringify({ runId: run.runId, trigger, probe: seen.probe, decision, failureClass: seen.failure }));
|
|
181
|
+
return decision;
|
|
160
182
|
},
|
|
161
183
|
};
|
|
162
184
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/chat",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "Jini's chat/artifact frontend, consolidated into one package with two subpaths: framework-free chat vocabulary and pure parsers in `/core`, headless hooks + presentational components + slots in `/react`. Import only the layer you need.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -303,11 +303,11 @@
|
|
|
303
303
|
"pg": "8.23.0",
|
|
304
304
|
"@ag-ui/core": "0.0.58",
|
|
305
305
|
"@jini-ai/db": "0.3.4",
|
|
306
|
-
"@jini-ai/protocol": "^0.4.4",
|
|
307
|
-
"@jini-ai/agent-runtime": "0.4.11",
|
|
308
|
-
"@jini-ai/ui": "^0.5.4",
|
|
309
306
|
"@jini-ai/core": "^0.4.6",
|
|
307
|
+
"@jini-ai/agent-runtime": "0.4.11",
|
|
310
308
|
"@jini-ai/diagnostics": "^0.5.3",
|
|
309
|
+
"@jini-ai/protocol": "^0.4.4",
|
|
310
|
+
"@jini-ai/ui": "^0.5.4",
|
|
311
311
|
"@jini-ai/agentic": "^0.4.4"
|
|
312
312
|
},
|
|
313
313
|
"typesVersions": {
|