ghostykit 0.7.1 → 0.8.1

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/README.md CHANGED
@@ -88,13 +88,14 @@ const ref = useRef<PilotoHandle>(null);
88
88
  <Piloto
89
89
  ref={ref}
90
90
  transport={transport}
91
- skin="easybits" // "ghosty" (default) | "easybits" | "neutral" | your own
91
+ skin="easybits" // "ghosty" (default) | "easybits" | "neutral" | "denik" | your own
92
92
  theme="auto" // "light" | "dark" | "auto"
93
93
  locale="en" // Spanish by default
94
94
  agentName="Admin assistant"
95
95
  welcome="How can I help with your crons?"
96
96
  suggestions={["Why did the monitor fail?"]}
97
97
  draft={{ text: "Why did the monitor fail?", key: row.id }} // prefilled + focused, not sent
98
+ background={running.length ? { label: "2 helpers working", since: oldestStart } : null} // work after the run closed
98
99
  threadId={conversationId} // reopen a stored thread
99
100
  context={() => ({ screen: "crons", selected })} // AG-UI `context`, every run
100
101
  onTurnDone={({ text }) => refreshMyData()}
@@ -107,7 +108,8 @@ ref.current?.send("Why did this fail?"); // ask on the user'
107
108
 
108
109
  Other props: `features` (`attachments`, `retry`, `copy`, `usage`, `replyOptions`, `duration`,
109
110
  `steer`, `narration`: `false` keeps the text written before each tool as part of the answer instead of
110
- checked progress lines), `references` / `onReferencesChange`, `placeholder`, `avatar`, `initialTurns`, `loadOlder`
111
+ checked progress lines), `icons` (`{ send?, stop? }`: your own nodes for the composer's round button;
112
+ default arrow and square), `references` / `onReferencesChange`, `placeholder`, `avatar`, `initialTurns`, `loadOlder`
111
113
  (earlier pages of a long thread), `forwardedProps` (extra keys for every run, e.g.
112
114
  `{permissions: "ask"}`), and the callbacks `onTurnStart`, `onTitle`, `onState`, `onEvent` (every raw
113
115
  AG-UI event), `onSend`. The agent's shared state (`STATE_SNAPSHOT` + `STATE_DELTA`) is
@@ -173,7 +175,7 @@ const PLUGINS = [crm, modelPicker({ onChange: (model) => api.setModel(model) }),
173
175
  | `composer.below` | Under the message box. | suggestion chips |
174
176
  | `composer.attachments` | `upload`, `transcribe`, `voiceNoteText`, and the limits `accept`, `maxFiles`, `maxBytes`. A voice note keeps its audio attached when there is `upload`. | `{ upload, accept: "image/*", maxFiles: 4, maxBytes: 4_000_000 }` |
175
177
  | `composer.replace` | `{ when(ctx), view }`: drawn instead of the message box while `when` is true. | trial over (403 + `x-needs-login`) → "Create your account" |
176
- | `header` | Top right, over the thread. | share button |
178
+ | `header` | The top bar, over the thread. A single view goes on the right; `{ start, end }` puts `start` on the left (who the agent is) and `end` on the right. | `{ start: avatar + name, end: new chat · close }` |
177
179
  | `sidePanel` | Next to the chat. | live page, subagents |
178
180
  | `aboveThread` | Above the messages. | a banner |
179
181
  | `empty` | The empty thread (instead of the greeting). | onboarding |
@@ -195,7 +197,7 @@ in the URL).
195
197
  - **Maps** (`strings`, `tools.verbs`, `tools.icons`, `vars`) are merged, and later plugins win. `markdown.transform`, `formatUserText` and `beforeSend` are chained.
196
198
  - **`events` and `state`** call every handler, in order.
197
199
  - **Keyed or single views** (`tools.render`, `message.custom`, `markdown.blocks`, `markdown.components`, `message.avatar`, `empty`, `waiting`, `composer.attachments`, `composer.replace`): the first plugin that declares one wins. A clash is a development warning.
198
- - **Lists** (`header`, `sidePanel`, `aboveThread`, `composer.actions`/`actionsEnd`/`above`/`below`, `message.user`/`assistant`/`assistantBefore`, `wrap`, `onTurnStart`/`onTurnDone`) all apply, in order.
200
+ - **Lists** (`header` and its `start`/`end`, `sidePanel`, `aboveThread`, `composer.actions`/`actionsEnd`/`above`/`below`, `message.user`/`assistant`/`assistantBefore`, `wrap`, `onTurnStart`/`onTurnDone`) all apply, in order.
199
201
 
200
202
  In development, ghostykit also warns once about a `CUSTOM` event that nothing handles.
201
203
 
@@ -228,6 +230,30 @@ like ghosty.studio; 16 px in the others). Every color, radius, border width and
228
230
  }
229
231
  ```
230
232
 
233
+ The variables a host (or a widget owner, inside an iframe) usually overrides:
234
+
235
+ | Variable | What it paints |
236
+ |---|---|
237
+ | `--piloto-font`, `--piloto-font-prose`, `--piloto-font-mono` | UI text, the answers' prose, code |
238
+ | `--piloto-rem` | The size unit inside the chat (`1rem`) |
239
+ | `--piloto-bg`, `--piloto-surface`, `--piloto-surface-2` | Background, cards and fields, hover/secondary |
240
+ | `--piloto-text`, `--piloto-text-2`, `--piloto-text-3` | Text, secondary text, hints |
241
+ | `--piloto-accent`, `--piloto-accent-strong`, `--piloto-accent-soft`, `--piloto-on-accent` | Brand color (buttons, focus, links), its pressed and soft tones, text on it |
242
+ | `--piloto-user-bubble`, `--piloto-user-text` | The user's bubble |
243
+ | `--piloto-composer-bg`, `--piloto-shadow-input` | The message box |
244
+ | `--piloto-border`, `--piloto-border-width` | Borders |
245
+ | `--piloto-radius-bubble`, `--piloto-radius-input`, `--piloto-radius-card`, `--piloto-radius-button` | Corners |
246
+ | `--piloto-radius-bubble-tail` | The bottom-right corner of the user's bubble (its "tail"); defaults to `--piloto-radius-bubble` |
247
+ | `--piloto-success`, `--piloto-danger`, `--piloto-warning`, `--piloto-warning-ink` | States, and the approval pass |
248
+
249
+ Set them in your CSS on `.piloto`, or from a plugin with `vars` (e.g. the owner's theme of an
250
+ embedded widget): `definePlugin({ name: "theme", vars: { "--piloto-accent": owner.color, "--piloto-radius-input": "8px" } })`.
251
+
252
+ The `denik` skin is Deník's Nik: blue `#5158F6`, ink `#11151A`, Satoshi (the host loads the font), a
253
+ dark user bubble with an 8 px tail, the answer without a bubble (add the avatar with
254
+ `message.avatar`), and a 36 px round send button that turns into an ink circle while stopping. Pair it
255
+ with `icons={{ send: <PaperPlane /> }}` and a plugin `header: { start, end }`.
256
+
231
257
  | Ghosty | EasyBits | Neutral |
232
258
  |---|---|---|
233
259
  | ![Ghosty skin](https://raw.githubusercontent.com/blissito/ghostykit/main/docs/screenshots/ghosty-light-mobile-2-permission.png) | ![EasyBits skin, dark](https://raw.githubusercontent.com/blissito/ghostykit/main/docs/screenshots/easybits-dark-mobile-2-permission.png) | ![Neutral skin](https://raw.githubusercontent.com/blissito/ghostykit/main/docs/screenshots/neutral-light-mobile-2-permission.png) |
@@ -1095,8 +1095,11 @@ function Composer({
1095
1095
  handleRef,
1096
1096
  accept,
1097
1097
  maxFiles = 8,
1098
- maxFileMb = 25
1098
+ maxFileMb = 25,
1099
+ icons
1099
1100
  }) {
1101
+ const sendIcon = icons?.send ?? /* @__PURE__ */ jsx2(ArrowUp, { className: "h-4 w-4" });
1102
+ const stopIcon = icons?.stop ?? /* @__PURE__ */ jsx2(Square, { className: "h-3 w-3 fill-current" });
1100
1103
  const { strings: t } = usePiloto();
1101
1104
  const [value, setValue] = useState(initialValue);
1102
1105
  const [files, setFiles] = useState([]);
@@ -1431,8 +1434,8 @@ function Composer({
1431
1434
  onClick: toggleRecording,
1432
1435
  "aria-label": t.stopAndSendVoice,
1433
1436
  title: t.send,
1434
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-brand-500 text-white hover:bg-brand-600",
1435
- children: /* @__PURE__ */ jsx2(ArrowUp, { className: "h-4 w-4" })
1437
+ className: "gc-send-voice flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-brand-500 text-white hover:bg-brand-600",
1438
+ children: sendIcon
1436
1439
  }
1437
1440
  )
1438
1441
  ] })
@@ -1517,8 +1520,9 @@ function Composer({
1517
1520
  onClick: busy ? onStop : () => void submit(),
1518
1521
  disabled: !busy && !canSend || uploading,
1519
1522
  "aria-label": busy ? t.stop : uploading ? t.uploading : t.send,
1523
+ "data-state": busy ? "stop" : "send",
1520
1524
  className: `gc-send flex h-8 w-8 items-center justify-center rounded-full transition-colors ${busy || canSend || uploading ? "bg-brand-500 text-white hover:bg-brand-600" : "bg-background-disabled text-text-disabled"}`,
1521
- children: busy ? /* @__PURE__ */ jsx2(Square, { className: "h-3 w-3 fill-current" }) : uploading ? /* @__PURE__ */ jsx2(LoaderCircle, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx2(ArrowUp, { className: "h-4 w-4" })
1525
+ children: busy ? stopIcon : uploading ? /* @__PURE__ */ jsx2(LoaderCircle, { className: "h-4 w-4 animate-spin" }) : sendIcon
1522
1526
  }
1523
1527
  )
1524
1528
  ] })
@@ -1580,6 +1584,9 @@ function ModelPicker({ models, current, onChange }) {
1580
1584
  }
1581
1585
 
1582
1586
  // src/react/plugins.ts
1587
+ function isView(x) {
1588
+ return typeof x === "function" || typeof x === "object" && x !== null && "$$typeof" in x;
1589
+ }
1583
1590
  function definePlugin(plugin) {
1584
1591
  return plugin;
1585
1592
  }
@@ -1618,7 +1625,13 @@ function pluginPoints(p) {
1618
1625
  add(p.composer?.above, "composer.above");
1619
1626
  add(p.composer?.below, "composer.below");
1620
1627
  add(p.composer?.attachments, "composer.attachments");
1621
- add(p.header, "header");
1628
+ if (p.header) {
1629
+ if (isView(p.header)) out.push("header");
1630
+ else {
1631
+ add(p.header.start, "header.start");
1632
+ add(p.header.end, "header.end");
1633
+ }
1634
+ }
1622
1635
  add(p.sidePanel, "sidePanel");
1623
1636
  add(p.aboveThread, "aboveThread");
1624
1637
  add(p.empty, "empty");
@@ -1646,6 +1659,7 @@ function resolvePlugins(list = []) {
1646
1659
  httpError: [],
1647
1660
  attachments: {},
1648
1661
  header: [],
1662
+ headerStart: [],
1649
1663
  sidePanel: [],
1650
1664
  aboveThread: [],
1651
1665
  composerActions: [],
@@ -1707,7 +1721,13 @@ function resolvePlugins(list = []) {
1707
1721
  }
1708
1722
  if (p.empty) r.empty ? r.warnings.push(`\xAB${p.name}\xBB tambi\xE9n dibuja empty: gana \xAB${r.empty.plugin}\xBB.`) : r.empty = own(p.empty);
1709
1723
  if (p.waiting) r.waiting ? r.warnings.push(`\xAB${p.name}\xBB tambi\xE9n dibuja waiting: gana \xAB${r.waiting.plugin}\xBB.`) : r.waiting = own(p.waiting);
1710
- if (p.header) r.header.push(own(p.header));
1724
+ if (p.header) {
1725
+ if (isView(p.header)) r.header.push(own(p.header));
1726
+ else {
1727
+ if (p.header.start) r.headerStart.push(own(p.header.start));
1728
+ if (p.header.end) r.header.push(own(p.header.end));
1729
+ }
1730
+ }
1711
1731
  if (p.sidePanel) r.sidePanel.push(own(p.sidePanel));
1712
1732
  if (p.aboveThread) r.aboveThread.push(own(p.aboveThread));
1713
1733
  if (p.composer?.actions) r.composerActions.push(own(p.composer.actions));
@@ -3,7 +3,7 @@ import {
3
3
  definePlugin,
4
4
  pluginPoints,
5
5
  pluginRequires
6
- } from "../chunks/chunk-PNU3LKWG.js";
6
+ } from "../chunks/chunk-VFCROLFF.js";
7
7
  import "../chunks/chunk-UUH37KXA.js";
8
8
  import "../chunks/chunk-6U545WJG.js";
9
9
 
@@ -52,8 +52,15 @@ export interface ComposerProps {
52
52
  accept?: string;
53
53
  maxFiles?: number;
54
54
  maxFileMb?: number;
55
+ /** Íconos del botón de mandar y de detener. Default: flecha hacia arriba y cuadrito. */
56
+ icons?: ComposerIcons;
55
57
  }
56
- export declare function Composer({ onSubmit, onStop, busy, canSteer, placeholder, initialValue, autoFocus, canSeeImages, attachments: withFiles, upload, transcribe, voiceNoteText, models, currentModel, onModelChange, references, onRemoveReference, onActivateReference, extras, extrasEnd, handleRef, accept, maxFiles, maxFileMb, }: ComposerProps): import("react").JSX.Element;
58
+ /** Los íconos propios del botón redondo del composer (`<Piloto icons={…} />`). */
59
+ export interface ComposerIcons {
60
+ send?: ReactNode;
61
+ stop?: ReactNode;
62
+ }
63
+ export declare function Composer({ onSubmit, onStop, busy, canSteer, placeholder, initialValue, autoFocus, canSeeImages, attachments: withFiles, upload, transcribe, voiceNoteText, models, currentModel, onModelChange, references, onRemoveReference, onActivateReference, extras, extrasEnd, handleRef, accept, maxFiles, maxFileMb, icons, }: ComposerProps): import("react").JSX.Element;
57
64
  /**
58
65
  * El selector de modelo. PORTADO de Ghosty (`ModelPicker.tsx`): dropdown discreto hacia arriba, sin
59
66
  * Radix; cierra con clic fuera y con Escape.
@@ -1,6 +1,7 @@
1
1
  import { type CSSProperties, type ReactNode } from "react";
2
2
  import type { Reference } from "../core/types";
3
3
  import type { PilotTransport } from "../transport";
4
+ import { type ComposerIcons } from "./Composer";
4
5
  import { type PilotTheme } from "./context";
5
6
  import { type PilotoPlugin, type PluginCtx } from "./plugins";
6
7
  import { type Pilot, type UsePilotOptions } from "./usePilot";
@@ -40,6 +41,8 @@ export interface PilotoProps extends UsePilotOptions {
40
41
  welcome?: ReactNode;
41
42
  avatar?: ReactNode;
42
43
  placeholder?: string;
44
+ /** Íconos del botón redondo del composer: `send` (mandar) y `stop` (detener). Default: flecha y cuadrito. */
45
+ icons?: ComposerIcons;
43
46
  suggestions?: string[];
44
47
  features?: PilotoFeatures;
45
48
  /**
@@ -50,6 +53,15 @@ export interface PilotoProps extends UsePilotOptions {
50
53
  text: string;
51
54
  key?: string | number;
52
55
  } | null;
56
+ /**
57
+ * Trabajo que sigue FUERA del run (ayudantes en segundo plano, una tarea larga): con el run ya
58
+ * cerrado, el hilo enseña `label` y cuenta desde `since` (ms) para que no parezca terminado.
59
+ * Con un run vivo manda su propia espera. `null` = nada pendiente.
60
+ */
61
+ background?: {
62
+ label: string;
63
+ since: number;
64
+ } | null;
53
65
  /** Referencias controladas (si no, Piloto las guarda; agrégalas con `ref.addReference`). */
54
66
  references?: Reference[];
55
67
  onReferencesChange?: (refs: Reference[]) => void;
@@ -1,6 +1,8 @@
1
1
  /** Los tres puntitos de «escribiendo» (color y animación en CSS: `gc-typing`). */
2
2
  export declare function TypingDots(): import("react").JSX.Element;
3
- export declare function WaitLabel({ since, showDuration, step, }: {
3
+ export declare function WaitLabel({ since, showDuration, step, text, }: {
4
+ /** Frase fija que gana a las del turno (p. ej. «2 ayudantes trabajando» con el run ya cerrado). */
5
+ text?: string | null;
4
6
  /** El paso con nombre en curso (`STEP_STARTED`): si `strings.steps` lo nombra, esa es la frase. */
5
7
  step?: string | null;
6
8
  /** Desde que se MANDÓ el mensaje (no desde que se montó la etiqueta). */
@@ -1,12 +1,12 @@
1
1
  export { Piloto, Piloto as default, type PilotoFeatures, type PilotoHandle, type PilotoProps } from "./Piloto";
2
- export { definePlugin, pluginPoints, pluginRequires, resolvePlugins, type PilotoPlugin, type PluginCtx, type PluginEvent, type PluginView, type ResolvedPlugins, } from "./plugins";
2
+ export { definePlugin, pluginPoints, pluginRequires, resolvePlugins, type PilotoPlugin, type PluginCtx, type PluginEvent, type PluginHeader, type PluginView, type ResolvedPlugins, } from "./plugins";
3
3
  export { toAgUiContext, usePilot, type Pilot, type SendOptions, type TurnDoneInfo, type TurnEvent, type UsePilotOptions } from "./usePilot";
4
4
  export { AgentBubble, CopyUserMessage, ReplyOptionButtons, TurnFooter, UsageStats, UserBubble, type AgentBubbleProps } from "./Bubble";
5
5
  export { AgentSteps, ArcadeIcon, ClawIcon, DEFAULT_TOOL_ICONS, Narration, RobotArmIcon, ToolRow } from "./Steps";
6
6
  export { LinkSafetyModal, Markdown } from "./Markdown";
7
7
  export { InlineMarkdown, renderInline } from "./InlineMarkdown";
8
8
  export { useTextAtPace } from "./TextAtPace";
9
- export { Composer, ModelPicker, mimeOf, type ComposerHandle, type ComposerProps, type ModelOption, type UploadResult } from "./Composer";
9
+ export { Composer, ModelPicker, mimeOf, type ComposerHandle, type ComposerIcons, type ComposerProps, type ModelOption, type UploadResult } from "./Composer";
10
10
  export { ConfirmCard, DecisionLines } from "./ConfirmCard";
11
11
  export { ReferenceChips, Suggestions } from "./Chips";
12
12
  export { TypingDots, WaitLabel } from "./Status";
@@ -80,7 +80,7 @@ import {
80
80
  resolvePlugins,
81
81
  rootProps,
82
82
  usePiloto
83
- } from "../chunks/chunk-PNU3LKWG.js";
83
+ } from "../chunks/chunk-VFCROLFF.js";
84
84
  import {
85
85
  TOOL_VERBS_EN,
86
86
  TOOL_VERBS_ES,
@@ -679,7 +679,7 @@ function UserBubble({ turn }) {
679
679
  },
680
680
  i
681
681
  )) }),
682
- (inBubble.length > 0 || visible.trim()) && /* @__PURE__ */ jsxs4("div", { className: "gc-msg-user max-w-[75%] whitespace-pre-wrap break-words rounded-(--piloto-radius-bubble) bg-(--piloto-user-bubble) px-4 py-3 font-sans text-base leading-[22px] text-(--piloto-user-text)", children: [
682
+ (inBubble.length > 0 || visible.trim()) && /* @__PURE__ */ jsxs4("div", { className: "gc-msg-user max-w-[75%] whitespace-pre-wrap break-words rounded-(--piloto-radius-bubble) rounded-br-[var(--piloto-radius-bubble-tail,var(--piloto-radius-bubble))] bg-(--piloto-user-bubble) px-4 py-3 font-sans text-base leading-[22px] text-(--piloto-user-text)", children: [
683
683
  inBubble.length > 0 && /* @__PURE__ */ jsx6("div", { className: `flex flex-wrap justify-end gap-1.5 ${visible.trim() ? "mb-2" : ""}`, children: inBubble.map(
684
684
  (a, i) => a.mimeType.startsWith("image/") ? /* @__PURE__ */ jsx6("img", { src: a.data ? `data:${a.mimeType};base64,${a.data}` : a.url, alt: "", className: "h-20 w-20 rounded-lg object-cover" }, i) : /* @__PURE__ */ jsx6("audio", { src: a.url, controls: true, preload: "metadata", className: "h-9 max-w-[16rem]" }, i)
685
685
  ) }),
@@ -828,7 +828,8 @@ function TypingDots() {
828
828
  function WaitLabel({
829
829
  since,
830
830
  showDuration = true,
831
- step
831
+ step,
832
+ text
832
833
  }) {
833
834
  const { strings: t } = usePiloto();
834
835
  const [mounted] = useState6(() => Date.now());
@@ -840,7 +841,7 @@ function WaitLabel({
840
841
  const seconds = Math.max(0, Math.floor((now - (since || mounted)) / 1e3));
841
842
  const phrases = t.thinkingPhrases;
842
843
  const stepText = step ? t.steps?.[step] : void 0;
843
- const what = stepText ? stepText : phrases?.length ? seconds >= STILL_WORKING_S ? t.stillWorking : phrases[Math.floor(seconds / 4) % phrases.length] : t.howItGoes(seconds);
844
+ const what = text ? text : stepText ? stepText : phrases?.length ? seconds >= STILL_WORKING_S ? t.stillWorking : phrases[Math.floor(seconds / 4) % phrases.length] : t.howItGoes(seconds);
844
845
  return /* @__PURE__ */ jsx7("div", { className: "flex flex-col gap-0.5 text-sm text-text-secondary", children: /* @__PURE__ */ jsxs5("p", { className: "flex items-center", children: [
845
846
  /* @__PURE__ */ jsx7(AnimatePresence2, { mode: "wait", initial: false, children: /* @__PURE__ */ jsx7(
846
847
  motion2.span,
@@ -1498,6 +1499,7 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1498
1499
  extras: plugins.composerActions.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.composerActions, ctx, styled: plugins.ghostykitStyled, inline: true }) : void 0,
1499
1500
  extrasEnd: plugins.composerActionsEnd.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.composerActionsEnd, ctx, styled: plugins.ghostykitStyled, inline: true }) : void 0,
1500
1501
  handleRef: composerHandle,
1502
+ icons: props.icons,
1501
1503
  accept,
1502
1504
  ...maxFiles ? { maxFiles } : {},
1503
1505
  ...maxBytes ? { maxFileMb: maxBytes / (1024 * 1024) } : {}
@@ -1511,7 +1513,12 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1511
1513
  ] }) : chat;
1512
1514
  const aboveComposer = plugins.composerAbove.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.composerAbove, ctx, styled: plugins.ghostykitStyled }) : null;
1513
1515
  const belowComposer = plugins.composerBelow.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.composerBelow, ctx, styled: plugins.ghostykitStyled }) : null;
1514
- const header = plugins.header.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.header, ctx, styled: plugins.ghostykitStyled, inline: true }) : null;
1516
+ const headerStart = plugins.headerStart.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.headerStart, ctx, styled: plugins.ghostykitStyled, inline: true }) : null;
1517
+ const headerEnd = plugins.header.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.header, ctx, styled: plugins.ghostykitStyled, inline: true }) : null;
1518
+ const headerSides = (headerStart || headerEnd) && /* @__PURE__ */ jsxs6(Fragment4, { children: [
1519
+ headerStart && /* @__PURE__ */ jsx8("div", { className: "piloto-header-start flex min-w-0 items-center gap-2", children: headerStart }),
1520
+ headerEnd && /* @__PURE__ */ jsx8("div", { className: "piloto-header-end ml-auto flex items-center gap-2", children: headerEnd })
1521
+ ] });
1515
1522
  const Empty = plugins.empty?.value;
1516
1523
  const Waiting = plugins.waiting?.value;
1517
1524
  const renderDeliveries = (turn, live) => {
@@ -1562,7 +1569,7 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1562
1569
  // Un hilo vacío enseña lo mismo que la portada: la cara, el saludo, la caja y por dónde
1563
1570
  // empezar. Si no cabe (panel bajo), se desplaza: `safe center` nunca empuja nada fuera.
1564
1571
  /* @__PURE__ */ jsxs6("div", { className: "gs-scroll flex min-h-0 flex-1 flex-col items-center justify-center overflow-y-auto px-4 pb-[10vh] [justify-content:safe_center]", children: [
1565
- header && /* @__PURE__ */ jsx8("div", { className: "absolute right-4 top-3 flex gap-2", children: header }),
1572
+ headerSides && /* @__PURE__ */ jsx8("div", { className: "piloto-header absolute inset-x-0 top-0 z-10 flex items-center gap-2 px-4 pt-3", children: headerSides }),
1566
1573
  Empty ? /* @__PURE__ */ jsx8("div", { ...mark(plugins.empty.plugin), children: /* @__PURE__ */ jsx8(Empty, { ctx }) }) : /* @__PURE__ */ jsxs6("div", { className: "mb-6 flex flex-col items-center", children: [
1567
1574
  props.avatar && /* @__PURE__ */ jsx8("div", { className: "mb-3", children: props.avatar }),
1568
1575
  props.welcome && // Un saludo largo (una explicación del piloto) baja a letra de lectura.
@@ -1582,7 +1589,7 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1582
1589
  ] })
1583
1590
  ] })
1584
1591
  ) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
1585
- header && /* @__PURE__ */ jsx8("div", { className: "flex shrink-0 justify-end gap-2 px-4 pt-3 sm:px-6", children: header }),
1592
+ headerSides && /* @__PURE__ */ jsx8("div", { className: "piloto-header flex shrink-0 items-center gap-2 px-4 pt-3 sm:px-6", children: headerSides }),
1586
1593
  /* @__PURE__ */ jsxs6(
1587
1594
  "div",
1588
1595
  {
@@ -1652,6 +1659,10 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1652
1659
  /* @__PURE__ */ jsx8(TypingDots, {}),
1653
1660
  /* @__PURE__ */ jsx8(WaitLabel, { since: state.turnStartedAt, step: state.step, showDuration: features.duration !== false })
1654
1661
  ] })),
1662
+ !busy && props.background && /* @__PURE__ */ jsxs6("div", { "data-background": true, className: "flex flex-col gap-1.5", children: [
1663
+ /* @__PURE__ */ jsx8(TypingDots, {}),
1664
+ /* @__PURE__ */ jsx8(WaitLabel, { text: props.background.label, since: props.background.since, showDuration: features.duration !== false })
1665
+ ] }),
1655
1666
  notice && /* @__PURE__ */ jsx8("p", { className: "text-sm text-text-tertiary", children: notice }),
1656
1667
  error && /* @__PURE__ */ jsx8("p", { className: "text-sm text-text-danger", children: error })
1657
1668
  ] }),
@@ -162,8 +162,11 @@ export interface PilotoPlugin {
162
162
  view: PluginView;
163
163
  };
164
164
  };
165
- /** Arriba a la derecha, sobre el hilo. */
166
- header?: PluginView;
165
+ /**
166
+ * La barra de arriba, sobre el hilo. Una vista sola va a la DERECHA (acciones); con
167
+ * `{ start, end }`, `start` va a la izquierda (avatar y nombre del agente) y `end` a la derecha.
168
+ */
169
+ header?: PluginView | PluginHeader;
167
170
  /** Junto al chat. */
168
171
  sidePanel?: PluginView;
169
172
  /** Arriba de los mensajes, dentro del hilo. */
@@ -187,6 +190,13 @@ export interface PilotoPlugin {
187
190
  /** Terminó un turno de este cliente (abrir la última entrega, re-leer datos). */
188
191
  onTurnDone?: (info: TurnDoneInfo, ctx: PluginCtx) => void;
189
192
  }
193
+ /** Los dos lados de la barra de arriba. */
194
+ export interface PluginHeader {
195
+ /** A la izquierda: quién es (avatar, nombre). */
196
+ start?: PluginView;
197
+ /** A la derecha: acciones (nueva conversación, cerrar). */
198
+ end?: PluginView;
199
+ }
190
200
  /** Ayuda de tipos: `definePlugin({ name: "crm", … })`. */
191
201
  export declare function definePlugin<T extends PilotoPlugin>(plugin: T): T;
192
202
  type Owned<T> = {
@@ -214,7 +224,10 @@ export interface ResolvedPlugins {
214
224
  attachments: NonNullable<NonNullable<PilotoPlugin["composer"]>["attachments"]>;
215
225
  empty?: Owned<PluginView>;
216
226
  waiting?: Owned<NonNullable<PilotoPlugin["waiting"]>>;
227
+ /** La barra de arriba, a la derecha (`header` o `header.end`). */
217
228
  header: Owned<PluginView>[];
229
+ /** La barra de arriba, a la izquierda (`header.start`). */
230
+ headerStart: Owned<PluginView>[];
218
231
  sidePanel: Owned<PluginView>[];
219
232
  aboveThread: Owned<PluginView>[];
220
233
  composerActions: Owned<PluginView>[];
package/dist/styles.css CHANGED
@@ -119,18 +119,21 @@
119
119
  .piloto .inset-0:not([data-piloto-isolate] *) {
120
120
  inset: 0px;
121
121
  }
122
+ .piloto .inset-x-0:not([data-piloto-isolate] *) {
123
+ inset-inline: 0px;
124
+ }
122
125
  .piloto .-top-12:not([data-piloto-isolate] *) {
123
126
  top: calc(var(--spacing) * -12);
124
127
  }
128
+ .piloto .top-0:not([data-piloto-isolate] *) {
129
+ top: 0px;
130
+ }
125
131
  .piloto .top-0\.5:not([data-piloto-isolate] *) {
126
132
  top: calc(var(--spacing) * 0.5);
127
133
  }
128
134
  .piloto .top-2:not([data-piloto-isolate] *) {
129
135
  top: calc(var(--spacing) * 2);
130
136
  }
131
- .piloto .top-3:not([data-piloto-isolate] *) {
132
- top: calc(var(--spacing) * 3);
133
- }
134
137
  .piloto .top-4:not([data-piloto-isolate] *) {
135
138
  top: calc(var(--spacing) * 4);
136
139
  }
@@ -254,6 +257,9 @@
254
257
  .piloto .ml-1:not([data-piloto-isolate] *) {
255
258
  margin-left: var(--spacing);
256
259
  }
260
+ .piloto .ml-auto:not([data-piloto-isolate] *) {
261
+ margin-left: auto;
262
+ }
257
263
  .piloto .block:not([data-piloto-isolate] *) {
258
264
  display: block;
259
265
  }
@@ -653,6 +659,9 @@
653
659
  .piloto .rounded-xl:not([data-piloto-isolate] *) {
654
660
  border-radius: var(--radius-xl);
655
661
  }
662
+ .piloto .rounded-br-\[var\(--piloto-radius-bubble-tail\,var\(--piloto-radius-bubble\)\)\]:not([data-piloto-isolate] *) {
663
+ border-bottom-right-radius: var(--piloto-radius-bubble-tail,var(--piloto-radius-bubble));
664
+ }
656
665
  .piloto .border:not([data-piloto-isolate] *) {
657
666
  border-style: var(--tw-border-style);
658
667
  border-width: 1px;
@@ -1853,7 +1862,7 @@
1853
1862
  `.gs-prosa`, la garra, los puntitos), (3) el blindaje contra el CSS de la app
1854
1863
  anfitriona y (4) la pieza nueva: el pase de permiso.
1855
1864
 
1856
- Pieles por `data-skin`: ghosty (default), easybits y neutral; cada una con su
1865
+ Pieles por `data-skin`: ghosty (default), easybits, neutral y denik; cada una con su
1857
1866
  oscuro (`data-theme="dark"`, o `auto` siguiendo al sistema).
1858
1867
  ───────────────────────────────────────────────────────────────────────────── */
1859
1868
 
@@ -1897,6 +1906,9 @@
1897
1906
 
1898
1907
  /* Forma (los valores de gs). */
1899
1908
  --piloto-radius-bubble: 12px;
1909
+ /* La esquina inferior derecha de TU burbuja (la «cola»). Si no se define, vale lo mismo que
1910
+ `--piloto-radius-bubble` (el componente usa ese respaldo). */
1911
+ /* --piloto-radius-bubble-tail: 12px; */
1900
1912
  --piloto-radius-input: 24px;
1901
1913
  --piloto-radius-card: 16px;
1902
1914
  --piloto-radius-button: 12px;
@@ -2055,6 +2067,80 @@
2055
2067
  --piloto-shadow-hero: 4px 4px 0 #000000;
2056
2068
  }
2057
2069
 
2070
+ /* ── Deník: azul #5158F6, tinta #11151A, Satoshi; tu burbuja oscura con cola, la respuesta sin
2071
+ burbuja (el avatar a la izquierda va por `message.avatar`). La app anfitriona carga Satoshi. ── */
2072
+ .piloto[data-skin="denik"] {
2073
+ --piloto-rem: 16px;
2074
+ --piloto-font: "Satoshi", "Satoshi ", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
2075
+ /* La respuesta va en la misma Satoshi, no en serif. */
2076
+ --piloto-font-prose: "Satoshi", "Satoshi ", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
2077
+ --piloto-bg: #ffffff;
2078
+ --piloto-surface: #f8f8fa;
2079
+ --piloto-surface-2: #f2f2f2;
2080
+ --piloto-surface-hover: #fafafa;
2081
+ --piloto-text: #11151a;
2082
+ --piloto-text-2: #4b5563;
2083
+ --piloto-text-3: #8391a1;
2084
+ --piloto-text-disabled: #c4cad2;
2085
+ --piloto-text-inverse: #ffffff;
2086
+ --piloto-inverse: #11151a;
2087
+ --piloto-border: #f2f2f2;
2088
+ --piloto-border-2: #f2f2f2;
2089
+ --piloto-accent-100: #eeeffe;
2090
+ --piloto-accent-soft: #a8abfa;
2091
+ --piloto-accent: #5158f6;
2092
+ --piloto-accent-strong: #3f46e0;
2093
+ --piloto-on-accent: #ffffff;
2094
+ --piloto-success: #10b981;
2095
+ --piloto-danger: #ca5757;
2096
+ --piloto-focus: #5158f6;
2097
+ --piloto-user-bubble: #11151a;
2098
+ --piloto-user-text: #ffffff;
2099
+ --piloto-composer-bg: #ffffff;
2100
+ --piloto-radius-bubble: 24px;
2101
+ --piloto-radius-bubble-tail: 8px;
2102
+ --piloto-radius-input: 24px;
2103
+ --piloto-radius-card: 16px;
2104
+ --piloto-radius-button: 12px;
2105
+ --piloto-border-width: 1px;
2106
+ --piloto-shadow-input: 0 4px 24px rgb(0 0 0 / 0.06);
2107
+ --piloto-shadow-color: #11151a;
2108
+ --piloto-shadow-hero: 5px 5px 0 #11151a;
2109
+ }
2110
+ .piloto[data-skin="denik"][data-theme="dark"] {
2111
+ --piloto-bg: #11151a;
2112
+ --piloto-surface: #1a1f26;
2113
+ --piloto-surface-2: #242a33;
2114
+ --piloto-surface-hover: #161b21;
2115
+ --piloto-text: #f2f4f7;
2116
+ --piloto-text-2: #c2c8d0;
2117
+ --piloto-text-3: #8391a1;
2118
+ --piloto-text-disabled: #4b5563;
2119
+ --piloto-text-inverse: #11151a;
2120
+ --piloto-inverse: #f2f4f7;
2121
+ --piloto-border: #242a33;
2122
+ --piloto-border-2: #1a1f26;
2123
+ --piloto-accent-100: #1c2140;
2124
+ --piloto-accent-soft: #3d43b8;
2125
+ --piloto-accent: #6b71f8;
2126
+ --piloto-accent-strong: #8a8ffa;
2127
+ --piloto-user-bubble: #2a303a;
2128
+ --piloto-user-text: #f2f4f7;
2129
+ --piloto-composer-bg: #161b21;
2130
+ --piloto-shadow-input: 0 4px 24px rgb(0 0 0 / 0.4);
2131
+ --piloto-shadow-color: #5158f6;
2132
+ --piloto-shadow-hero: 5px 5px 0 #5158f6;
2133
+ }
2134
+ /* Las piezas de Deník que no son sólo color. */
2135
+ .piloto[data-skin="denik"] .piloto-header:not([data-piloto-isolate] *) { padding: 12px 16px; border-bottom: 1px solid var(--piloto-border); background: var(--piloto-bg); }
2136
+ .piloto[data-skin="denik"] .gc-msg-user:not([data-piloto-isolate] *) { max-width: 85%; padding: 10px 16px; font-size: 14px; line-height: 20px; }
2137
+ .piloto[data-skin="denik"] .gc-input:focus-within:not([data-piloto-isolate] *) { border-color: var(--piloto-accent); }
2138
+ /* El botón redondo: 36 px; apagado sigue azul (más tenue); al responder, círculo de tinta. */
2139
+ .piloto[data-skin="denik"] .gc-send:not([data-piloto-isolate] *),
2140
+ .piloto[data-skin="denik"] .gc-send-voice:not([data-piloto-isolate] *) { width: 36px; height: 36px; }
2141
+ .piloto[data-skin="denik"] .gc-send[data-state="send"]:disabled:not([data-piloto-isolate] *) { background: var(--piloto-accent); color: var(--piloto-on-accent); opacity: 0.4; }
2142
+ .piloto[data-skin="denik"] .gc-send[data-state="stop"]:not([data-piloto-isolate] *) { background: var(--piloto-inverse); color: var(--piloto-text-inverse); }
2143
+
2058
2144
  @media (prefers-color-scheme: dark) {
2059
2145
  .piloto[data-theme="auto"] {
2060
2146
  color-scheme: dark;
@@ -2123,6 +2209,30 @@
2123
2209
  --piloto-shadow-color: #000000;
2124
2210
  --piloto-shadow-hero: 4px 4px 0 #000000;
2125
2211
  }
2212
+ .piloto[data-skin="denik"][data-theme="auto"] {
2213
+ --piloto-bg: #11151a;
2214
+ --piloto-surface: #1a1f26;
2215
+ --piloto-surface-2: #242a33;
2216
+ --piloto-surface-hover: #161b21;
2217
+ --piloto-text: #f2f4f7;
2218
+ --piloto-text-2: #c2c8d0;
2219
+ --piloto-text-3: #8391a1;
2220
+ --piloto-text-disabled: #4b5563;
2221
+ --piloto-text-inverse: #11151a;
2222
+ --piloto-inverse: #f2f4f7;
2223
+ --piloto-border: #242a33;
2224
+ --piloto-border-2: #1a1f26;
2225
+ --piloto-accent-100: #1c2140;
2226
+ --piloto-accent-soft: #3d43b8;
2227
+ --piloto-accent: #6b71f8;
2228
+ --piloto-accent-strong: #8a8ffa;
2229
+ --piloto-user-bubble: #2a303a;
2230
+ --piloto-user-text: #f2f4f7;
2231
+ --piloto-composer-bg: #161b21;
2232
+ --piloto-shadow-input: 0 4px 24px rgb(0 0 0 / 0.4);
2233
+ --piloto-shadow-color: #5158f6;
2234
+ --piloto-shadow-hero: 5px 5px 0 #5158f6;
2235
+ }
2126
2236
  }
2127
2237
 
2128
2238
  /* ── Reset con alcance y BLINDAJE contra el CSS de la app anfitriona ──────────
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ghostykit",
3
- "version": "0.7.1",
4
- "description": "Ghosty Autopilot \u2014 the autopilot chat for your React app, on AG-UI: works with LangGraph, Mastra, CrewAI or any AG-UI agent. Streaming text, agent steps, human approval cards (interrupt/resume), attachments and voice.",
3
+ "version": "0.8.1",
4
+ "description": "Ghosty Autopilot — the autopilot chat for your React app, on AG-UI: works with LangGraph, Mastra, CrewAI or any AG-UI agent. Streaming text, agent steps, human approval cards (interrupt/resume), attachments and voice.",
5
5
  "type": "module",
6
6
  "main": "./dist/core/index.js",
7
7
  "types": "./dist/core/index.d.ts",