ghostykit 0.7.0 → 0.8.0

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,7 +88,7 @@ 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"
@@ -106,7 +106,9 @@ ref.current?.send("Why did this fail?"); // ask on the user'
106
106
  ```
107
107
 
108
108
  Other props: `features` (`attachments`, `retry`, `copy`, `usage`, `replyOptions`, `duration`,
109
- `steer`), `references` / `onReferencesChange`, `placeholder`, `avatar`, `initialTurns`, `loadOlder`
109
+ `steer`, `narration`: `false` keeps the text written before each tool as part of the answer instead of
110
+ checked progress lines), `icons` (`{ send?, stop? }`: your own nodes for the composer's round button;
111
+ default arrow and square), `references` / `onReferencesChange`, `placeholder`, `avatar`, `initialTurns`, `loadOlder`
110
112
  (earlier pages of a long thread), `forwardedProps` (extra keys for every run, e.g.
111
113
  `{permissions: "ask"}`), and the callbacks `onTurnStart`, `onTitle`, `onState`, `onEvent` (every raw
112
114
  AG-UI event), `onSend`. The agent's shared state (`STATE_SNAPSHOT` + `STATE_DELTA`) is
@@ -152,6 +154,7 @@ const PLUGINS = [crm, modelPicker({ onChange: (model) => api.setModel(model) }),
152
154
  | `strings` | Texts over the locale's. | `{ stop: "Para" }` |
153
155
  | `markdown.transform` | Rewrites the answer's markdown, with the turn at hand (chained). | `(md, turn) => md.replace(/#(\d+)/g, "[#$1](#folio-$1)")` |
154
156
  | `markdown.onLinkClick` | A click on a link; `true` if handled (asked in order). | open in-app |
157
+ | `markdown.components` | How a markdown element is drawn, by tag (gets its props and `ctx`): e.g. a link that is a CTA as a button. | `{ a: ({ href, children, ctx }) => … }` |
155
158
  | `markdown.blocks` | Fenced blocks by name (```` ```eb-file ````) taken out of the text and drawn as cards under the answer; one still being written comes with `complete: false`. | `{ "eb-file": FileCard }` |
156
159
  | `vars`, `className` | Branding on the root: `--piloto-*` tokens and classes. | `{ "--piloto-radius-bubble": "6px" }` |
157
160
  | `tools.verbs` | How each tool is named in the steps. | `{ run_cron: { one, many, running } }` |
@@ -163,17 +166,21 @@ const PLUGINS = [crm, modelPicker({ onChange: (model) => api.setModel(model) }),
163
166
  | `message.assistantBefore` | Before each answer's text, under its steps. | live page progress, "Creating image…" |
164
167
  | `message.user` / `message.assistant` | Extras under each user message / answer. | `({ turn, live }) => …` |
165
168
  | `message.formatUserText` | How the user's text is shown (chained). | strip app plumbing |
169
+ | `message.beforeSend` | Rewrites the text that goes out (chained). | prepend what was pointed at |
170
+ | `message.avatar` | The agent's avatar next to each answer. | `() => <img src={logo} />` |
166
171
  | `composer.actions` | Buttons in the composer bar, between the image button and the clip. | "My files", "Point" |
167
172
  | `composer.actionsEnd` | Buttons after the clip. | model picker |
168
173
  | `composer.above` | Above the message box. | `<Subagents pilot={ctx.pilot} />` |
169
174
  | `composer.below` | Under the message box. | suggestion chips |
170
- | `composer.attachments` | `upload`, `transcribe`, `voiceNoteText`. | `{ upload: async (f) => ({ url }) }` |
171
- | `header` | Top right, over the thread. | share button |
175
+ | `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 }` |
176
+ | `composer.replace` | `{ when(ctx), view }`: drawn instead of the message box while `when` is true. | trial over (403 + `x-needs-login`) → "Create your account" |
177
+ | `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 }` |
172
178
  | `sidePanel` | Next to the chat. | live page, subagents |
173
179
  | `aboveThread` | Above the messages. | a banner |
174
180
  | `empty` | The empty thread (instead of the greeting). | onboarding |
175
181
  | `waiting` | While the agent hasn't answered (instead of the waiting phrase). Gets `step`, `since`. | "Creating your image…" |
176
182
  | `wrap` | A provider around the whole chat (`{ctx, children}`); the first plugin is outermost. | artifacts panel |
183
+ | `onHttpError` | The server refused a message before answering (`{status, code, message, headers}`; also in `pilot.state.httpError` until the next send). | 429 → upgrade banner |
177
184
  | `onTurnStart` / `onTurnDone` | This client started / finished a turn. | open the last delivery |
178
185
 
179
186
  Every view gets `ctx`: `{pilot, agentState, strings, send, addReference, addAttachments, setDraft, focus, plugins}`.
@@ -186,10 +193,10 @@ in the URL).
186
193
 
187
194
  **Order and precedence**, in the order of the `plugins` list:
188
195
 
189
- - **Maps** (`strings`, `tools.verbs`, `tools.icons`, `vars`) are merged, and later plugins win. `markdown` and `formatUserText` are chained.
196
+ - **Maps** (`strings`, `tools.verbs`, `tools.icons`, `vars`) are merged, and later plugins win. `markdown.transform`, `formatUserText` and `beforeSend` are chained.
190
197
  - **`events` and `state`** call every handler, in order.
191
- - **Keyed or single views** (`tools.render`, `message.custom`, `markdown.blocks`, `empty`, `waiting`, `composer.attachments`): the first plugin that declares one wins. A clash is a development warning.
192
- - **Lists** (`header`, `sidePanel`, `aboveThread`, `composer.actions`/`actionsEnd`/`above`/`below`, `message.user`/`assistant`/`assistantBefore`, `wrap`, `onTurnStart`/`onTurnDone`) all apply, in order.
198
+ - **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.
199
+ - **Lists** (`header` and its `start`/`end`, `sidePanel`, `aboveThread`, `composer.actions`/`actionsEnd`/`above`/`below`, `message.user`/`assistant`/`assistantBefore`, `wrap`, `onTurnStart`/`onTurnDone`) all apply, in order.
193
200
 
194
201
  In development, ghostykit also warns once about a `CUSTOM` event that nothing handles.
195
202
 
@@ -222,6 +229,30 @@ like ghosty.studio; 16 px in the others). Every color, radius, border width and
222
229
  }
223
230
  ```
224
231
 
232
+ The variables a host (or a widget owner, inside an iframe) usually overrides:
233
+
234
+ | Variable | What it paints |
235
+ |---|---|
236
+ | `--piloto-font`, `--piloto-font-prose`, `--piloto-font-mono` | UI text, the answers' prose, code |
237
+ | `--piloto-rem` | The size unit inside the chat (`1rem`) |
238
+ | `--piloto-bg`, `--piloto-surface`, `--piloto-surface-2` | Background, cards and fields, hover/secondary |
239
+ | `--piloto-text`, `--piloto-text-2`, `--piloto-text-3` | Text, secondary text, hints |
240
+ | `--piloto-accent`, `--piloto-accent-strong`, `--piloto-accent-soft`, `--piloto-on-accent` | Brand color (buttons, focus, links), its pressed and soft tones, text on it |
241
+ | `--piloto-user-bubble`, `--piloto-user-text` | The user's bubble |
242
+ | `--piloto-composer-bg`, `--piloto-shadow-input` | The message box |
243
+ | `--piloto-border`, `--piloto-border-width` | Borders |
244
+ | `--piloto-radius-bubble`, `--piloto-radius-input`, `--piloto-radius-card`, `--piloto-radius-button` | Corners |
245
+ | `--piloto-radius-bubble-tail` | The bottom-right corner of the user's bubble (its "tail"); defaults to `--piloto-radius-bubble` |
246
+ | `--piloto-success`, `--piloto-danger`, `--piloto-warning`, `--piloto-warning-ink` | States, and the approval pass |
247
+
248
+ Set them in your CSS on `.piloto`, or from a plugin with `vars` (e.g. the owner's theme of an
249
+ embedded widget): `definePlugin({ name: "theme", vars: { "--piloto-accent": owner.color, "--piloto-radius-input": "8px" } })`.
250
+
251
+ The `denik` skin is Deník's Nik: blue `#5158F6`, ink `#11151A`, Satoshi (the host loads the font), a
252
+ dark user bubble with an 8 px tail, the answer without a bubble (add the avatar with
253
+ `message.avatar`), and a 36 px round send button that turns into an ink circle while stopping. Pair it
254
+ with `icons={{ send: <PaperPlane /> }}` and a plugin `header: { start, end }`.
255
+
225
256
  | Ghosty | EasyBits | Neutral |
226
257
  |---|---|---|
227
258
  | ![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) |
@@ -439,7 +439,7 @@ function finish(s0, e, now, failed = false) {
439
439
  }
440
440
  function beginSend(prev, text, attachments = [], now = Date.now()) {
441
441
  const turns = text || attachments.length ? [...prev.turns, { role: "user", text, ...attachments.length ? { attachments } : {} }] : prev.turns;
442
- return { ...prev, turns, busy: true, error: null, notice: null, cutRun: null, turnStartedAt: now, effects: [{ type: "turn-start" }] };
442
+ return { ...prev, turns, busy: true, error: null, notice: null, cutRun: null, httpError: null, turnStartedAt: now, effects: [{ type: "turn-start" }] };
443
443
  }
444
444
  function beginSteer(prev, text) {
445
445
  return { ...prev, turns: [...prev.turns, { role: "user", text }], notice: null, openText: null, effects: [] };
@@ -1,11 +1,12 @@
1
1
  // src/transport/index.ts
2
2
  var PilotHttpError = class extends Error {
3
- constructor(message, status, code, retryAfter) {
3
+ constructor(message, status, code, retryAfter, headers) {
4
4
  super(message);
5
5
  this.name = "PilotHttpError";
6
6
  this.status = status;
7
7
  this.code = code;
8
8
  this.retryAfter = retryAfter;
9
+ this.headers = headers;
9
10
  }
10
11
  };
11
12
  async function readSse(body, onMessage) {
@@ -62,7 +63,9 @@ function createAgUiTransport(opts) {
62
63
  }
63
64
  if (!res.ok) {
64
65
  const json = await res.json().catch(() => null);
65
- throw new PilotHttpError(json?.message ?? json?.error ?? `Error ${res.status}`, res.status, json?.error, retryAfter);
66
+ const headers = {};
67
+ res.headers?.forEach?.((v, k) => void (headers[k.toLowerCase()] = v));
68
+ throw new PilotHttpError(json?.message ?? json?.error ?? `Error ${res.status}`, res.status, json?.error, retryAfter, headers);
66
69
  }
67
70
  if (!res.body) return;
68
71
  const serverDate = Date.parse(res.headers.get("date") ?? "");
@@ -942,8 +942,25 @@ var __iconData37 = {
942
942
  __iconData37.node;
943
943
  var UsersRound = createLucideIcon(__iconData37);
944
944
 
945
- // ../../node_modules/lucide-react/dist/esm/icons/x.mjs
945
+ // ../../node_modules/lucide-react/dist/esm/icons/wrench.mjs
946
946
  var __iconData38 = {
947
+ name: "wrench",
948
+ size: 24,
949
+ node: [
950
+ [
951
+ "path",
952
+ {
953
+ d: "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z",
954
+ key: "1ngwbx"
955
+ }
956
+ ]
957
+ ]
958
+ };
959
+ __iconData38.node;
960
+ var Wrench = createLucideIcon(__iconData38);
961
+
962
+ // ../../node_modules/lucide-react/dist/esm/icons/x.mjs
963
+ var __iconData39 = {
947
964
  name: "x",
948
965
  size: 24,
949
966
  node: [
@@ -951,8 +968,8 @@ var __iconData38 = {
951
968
  ["path", { d: "m6 6 12 12", key: "d8bk6v" }]
952
969
  ]
953
970
  };
954
- __iconData38.node;
955
- var X = createLucideIcon(__iconData38);
971
+ __iconData39.node;
972
+ var X = createLucideIcon(__iconData39);
956
973
 
957
974
  // src/react/Chips.tsx
958
975
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -1033,6 +1050,9 @@ function mimeOf(file) {
1033
1050
  const ext = (file.name.match(/\.([A-Za-z0-9]+)$/)?.[1] || "").toLowerCase();
1034
1051
  return BY_EXTENSION[ext] ?? "application/octet-stream";
1035
1052
  }
1053
+ function accepts(accept, name, mime) {
1054
+ return accept.split(",").map((x) => x.trim().toLowerCase()).filter(Boolean).some((rule) => rule.startsWith(".") ? name.toLowerCase().endsWith(rule) : rule.endsWith("/*") ? mime.startsWith(rule.slice(0, -1)) : mime === rule);
1055
+ }
1036
1056
  var isText = (mime) => mime.startsWith("text/") || mime === "application/json" || mime.endsWith("+json") || mime.endsWith("+xml");
1037
1057
  function shortSize(bytes) {
1038
1058
  if (bytes < 1024) return `${bytes} B`;
@@ -1073,9 +1093,13 @@ function Composer({
1073
1093
  extras,
1074
1094
  extrasEnd,
1075
1095
  handleRef,
1096
+ accept,
1076
1097
  maxFiles = 8,
1077
- maxFileMb = 25
1098
+ maxFileMb = 25,
1099
+ icons
1078
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" });
1079
1103
  const { strings: t } = usePiloto();
1080
1104
  const [value, setValue] = useState(initialValue);
1081
1105
  const [files, setFiles] = useState([]);
@@ -1129,6 +1153,10 @@ function Composer({
1129
1153
  rejected.push(t.tooBig(file.name, maxFileMb));
1130
1154
  continue;
1131
1155
  }
1156
+ if (accept && !accepts(accept, file.name, mime)) {
1157
+ rejected.push(t.cantUpload(file.name));
1158
+ continue;
1159
+ }
1132
1160
  if (next.length >= maxFiles) {
1133
1161
  rejected.push(t.tooMany(maxFiles));
1134
1162
  break;
@@ -1150,7 +1178,7 @@ function Composer({
1150
1178
  setNotice(rejected.length ? rejected.join(" \xB7 ") : null);
1151
1179
  ref.current?.focus({ preventScroll: true });
1152
1180
  },
1153
- [busy, canSeeImages, upload, maxFiles, maxFileMb, t]
1181
+ [busy, canSeeImages, upload, maxFiles, maxFileMb, accept, t]
1154
1182
  );
1155
1183
  useEffect(() => {
1156
1184
  if (!withFiles) return;
@@ -1406,8 +1434,8 @@ function Composer({
1406
1434
  onClick: toggleRecording,
1407
1435
  "aria-label": t.stopAndSendVoice,
1408
1436
  title: t.send,
1409
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-brand-500 text-white hover:bg-brand-600",
1410
- 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
1411
1439
  }
1412
1440
  )
1413
1441
  ] })
@@ -1434,7 +1462,7 @@ function Composer({
1434
1462
  /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-2", children: [
1435
1463
  /* @__PURE__ */ jsxs2("div", { className: "@container flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden", children: [
1436
1464
  withFiles && /* @__PURE__ */ jsxs2(Fragment2, { children: [
1437
- /* @__PURE__ */ jsx2(
1465
+ (!accept || accepts(accept, "x.png", "image/png")) && /* @__PURE__ */ jsx2(
1438
1466
  "button",
1439
1467
  {
1440
1468
  type: "button",
@@ -1451,13 +1479,13 @@ function Composer({
1451
1479
  }
1452
1480
  ),
1453
1481
  extras,
1454
- /* @__PURE__ */ jsx2(
1482
+ (!accept || !/^\s*image\/\*\s*$/.test(accept)) && /* @__PURE__ */ jsx2(
1455
1483
  "button",
1456
1484
  {
1457
1485
  type: "button",
1458
1486
  onClick: () => {
1459
1487
  if (!fileRef.current) return;
1460
- fileRef.current.accept = "";
1488
+ fileRef.current.accept = accept ?? "";
1461
1489
  fileRef.current.click();
1462
1490
  },
1463
1491
  disabled: busy || files.length >= maxFiles,
@@ -1492,8 +1520,9 @@ function Composer({
1492
1520
  onClick: busy ? onStop : () => void submit(),
1493
1521
  disabled: !busy && !canSend || uploading,
1494
1522
  "aria-label": busy ? t.stop : uploading ? t.uploading : t.send,
1523
+ "data-state": busy ? "stop" : "send",
1495
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"}`,
1496
- 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
1497
1526
  }
1498
1527
  )
1499
1528
  ] })
@@ -1555,6 +1584,9 @@ function ModelPicker({ models, current, onChange }) {
1555
1584
  }
1556
1585
 
1557
1586
  // src/react/plugins.ts
1587
+ function isView(x) {
1588
+ return typeof x === "function" || typeof x === "object" && x !== null && "$$typeof" in x;
1589
+ }
1558
1590
  function definePlugin(plugin) {
1559
1591
  return plugin;
1560
1592
  }
@@ -1583,12 +1615,23 @@ function pluginPoints(p) {
1583
1615
  add(p.message?.assistant, "message.assistant");
1584
1616
  for (const k of Object.keys(p.message?.custom ?? {})) out.push(`message.custom[${k}]`);
1585
1617
  add(p.message?.formatUserText, "message.formatUserText");
1618
+ add(p.message?.beforeSend, "message.beforeSend");
1619
+ add(p.message?.avatar, "message.avatar");
1620
+ for (const k of Object.keys(p.markdown?.components ?? {})) out.push(`markdown.components[${k}]`);
1621
+ add(p.composer?.replace, "composer.replace");
1622
+ add(p.onHttpError, "onHttpError");
1586
1623
  add(p.composer?.actions, "composer.actions");
1587
1624
  add(p.composer?.actionsEnd, "composer.actionsEnd");
1588
1625
  add(p.composer?.above, "composer.above");
1589
1626
  add(p.composer?.below, "composer.below");
1590
1627
  add(p.composer?.attachments, "composer.attachments");
1591
- 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
+ }
1592
1635
  add(p.sidePanel, "sidePanel");
1593
1636
  add(p.aboveThread, "aboveThread");
1594
1637
  add(p.empty, "empty");
@@ -1611,8 +1654,12 @@ function resolvePlugins(list = []) {
1611
1654
  toolRenders: {},
1612
1655
  customRenders: {},
1613
1656
  blockRenders: {},
1657
+ components: {},
1658
+ replacers: [],
1659
+ httpError: [],
1614
1660
  attachments: {},
1615
1661
  header: [],
1662
+ headerStart: [],
1616
1663
  sidePanel: [],
1617
1664
  aboveThread: [],
1618
1665
  composerActions: [],
@@ -1633,6 +1680,7 @@ function resolvePlugins(list = []) {
1633
1680
  const transforms = [];
1634
1681
  const linkHandlers = [];
1635
1682
  const formatters = [];
1683
+ const beforeSends = [];
1636
1684
  const names = /* @__PURE__ */ new Set();
1637
1685
  const first = (slot, key, plugin, value, point) => {
1638
1686
  if (slot[key]) r.warnings.push(`\xAB${plugin}\xBB y \xAB${slot[key].plugin}\xBB dibujan ${point}[${key}]: gana \xAB${slot[key].plugin}\xBB (va antes).`);
@@ -1654,13 +1702,18 @@ function resolvePlugins(list = []) {
1654
1702
  for (const [k, v] of Object.entries(p.message?.custom ?? {})) first(r.customRenders, k, p.name, v, "message.custom");
1655
1703
  for (const [k, v] of Object.entries(p.markdown?.blocks ?? {})) first(r.blockRenders, k, p.name, v, "markdown.blocks");
1656
1704
  if (p.styles === "ghostykit") r.ghostykitStyled.add(p.name);
1705
+ for (const [k, v] of Object.entries(p.markdown?.components ?? {})) first(r.components, k, p.name, v, "markdown.components");
1706
+ if (p.message?.avatar) r.avatar ? r.warnings.push(`\xAB${p.name}\xBB tambi\xE9n da message.avatar: gana \xAB${r.avatar.plugin}\xBB.`) : r.avatar = own(p.message.avatar);
1707
+ if (p.message?.beforeSend) beforeSends.push(p.message.beforeSend);
1708
+ if (p.composer?.replace) r.replacers.push(own(p.composer.replace));
1709
+ if (p.onHttpError) r.httpError.push(own(p.onHttpError));
1657
1710
  if (p.composer?.actionsEnd) r.composerActionsEnd.push(own(p.composer.actionsEnd));
1658
1711
  if (p.composer?.below) r.composerBelow.push(own(p.composer.below));
1659
1712
  if (p.message?.assistantBefore) r.messageBefore.push(own(p.message.assistantBefore));
1660
1713
  if (p.wrap) r.wraps.push(own(p.wrap));
1661
1714
  if (p.onTurnStart) r.turnStart.push(own(p.onTurnStart));
1662
1715
  if (p.onTurnDone) r.turnDone.push(own(p.onTurnDone));
1663
- for (const key of ["upload", "transcribe", "voiceNoteText"]) {
1716
+ for (const key of ["upload", "transcribe", "voiceNoteText", "accept", "maxFiles", "maxBytes"]) {
1664
1717
  const v = p.composer?.attachments?.[key];
1665
1718
  if (!v) continue;
1666
1719
  if (r.attachments[key]) r.warnings.push(`\xAB${p.name}\xBB tambi\xE9n da composer.attachments.${key}: gana el primero.`);
@@ -1668,7 +1721,13 @@ function resolvePlugins(list = []) {
1668
1721
  }
1669
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);
1670
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);
1671
- 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
+ }
1672
1731
  if (p.sidePanel) r.sidePanel.push(own(p.sidePanel));
1673
1732
  if (p.aboveThread) r.aboveThread.push(own(p.aboveThread));
1674
1733
  if (p.composer?.actions) r.composerActions.push(own(p.composer.actions));
@@ -1685,6 +1744,7 @@ function resolvePlugins(list = []) {
1685
1744
  }
1686
1745
  if (transforms.length) r.markdown.transform = (md, turn) => transforms.reduce((acc, f) => f(acc, turn), md);
1687
1746
  if (linkHandlers.length) r.markdown.onLinkClick = (href, ev) => linkHandlers.some((f) => f(href, ev));
1747
+ if (beforeSends.length) r.beforeSend = (t, ctx) => beforeSends.reduce((acc, f) => f(acc, ctx), t);
1688
1748
  if (formatters.length) r.formatUserText = (t) => formatters.reduce((acc, f) => f(acc), t);
1689
1749
  return r;
1690
1750
  }
@@ -1736,6 +1796,7 @@ export {
1736
1796
  Send,
1737
1797
  ShieldAlert,
1738
1798
  UsersRound,
1799
+ Wrench,
1739
1800
  X,
1740
1801
  STRINGS_ES,
1741
1802
  STRINGS_EN,
@@ -24,7 +24,7 @@ import {
24
24
  subagentsOf,
25
25
  takeQueued,
26
26
  toPermission
27
- } from "../chunks/chunk-FWBUIGTC.js";
27
+ } from "../chunks/chunk-7M2B7VKH.js";
28
28
  import {
29
29
  TOOL_VERBS_EN,
30
30
  TOOL_VERBS_ES,
@@ -159,6 +159,13 @@ export interface PilotState {
159
159
  /** Cuándo empezó el turno que ESTE cliente encargó (0 = ninguno). */
160
160
  turnStartedAt: number;
161
161
  runId: string | null;
162
+ /** El último rechazo HTTP del servidor (antes del stream): para avisos propios (p. ej. 403 + `x-needs-login`). Se limpia al mandar. */
163
+ httpError?: {
164
+ status: number;
165
+ code?: string;
166
+ message: string;
167
+ headers?: Record<string, string>;
168
+ } | null;
162
169
  /** El estado compartido del agente (`STATE_SNAPSHOT`, con `STATE_DELTA` aplicado encima). */
163
170
  agentState: Record<string, unknown>;
164
171
  /** El paso con nombre en curso (`STEP_STARTED`), p. ej. `waking`. */
@@ -3,7 +3,7 @@ import {
3
3
  definePlugin,
4
4
  pluginPoints,
5
5
  pluginRequires
6
- } from "../chunks/chunk-ZIUOXG7B.js";
6
+ } from "../chunks/chunk-VFCROLFF.js";
7
7
  import "../chunks/chunk-UUH37KXA.js";
8
8
  import "../chunks/chunk-6U545WJG.js";
9
9
 
@@ -40,10 +40,14 @@ export interface AgentBubbleProps {
40
40
  renderTools?: (turn: Turn, live: boolean) => ReactNode;
41
41
  /** Antes del texto de la respuesta, bajo sus pasos. */
42
42
  renderBefore?: (turn: Turn, live: boolean) => ReactNode;
43
+ /** `false`: el texto antes de cada herramienta NO se vuelve paso con palomita (todo es respuesta). */
44
+ narration?: boolean;
45
+ /** El avatar del agente a la izquierda de la respuesta. */
46
+ avatar?: ReactNode;
43
47
  /** Bloques con nombre (```eb-file```) que salen del texto y se pintan como tarjetas bajo la respuesta. */
44
48
  blocks?: {
45
49
  langs: string[];
46
50
  render: (blocks: TextBlock[], turn: Turn, live: boolean) => ReactNode;
47
51
  };
48
52
  }
49
- export declare function AgentBubble({ turn, live, onRetry, onOption, showUsage, showCopy, showDuration, renderDeliveries, renderTools, renderBefore, blocks }: AgentBubbleProps): import("react").JSX.Element;
53
+ export declare function AgentBubble(props: AgentBubbleProps): import("react").JSX.Element;
@@ -16,6 +16,8 @@ export interface ComposerHandle {
16
16
  }
17
17
  /** El tipo del archivo, deducido del nombre cuando el navegador no lo dice (pasa más de lo que parece). */
18
18
  export declare function mimeOf(file: File): string;
19
+ /** ¿Cabe en un `accept` («image/*,.pdf,application/json»)? */
20
+ export declare function accepts(accept: string, name: string, mime: string): boolean;
19
21
  export interface ComposerProps {
20
22
  onSubmit: (text: string, attachments: Attachment[]) => void;
21
23
  onStop?: () => void;
@@ -46,10 +48,19 @@ export interface ComposerProps {
46
48
  extrasEnd?: ReactNode;
47
49
  /** Para agregar adjuntos desde afuera. */
48
50
  handleRef?: Ref<ComposerHandle>;
51
+ /** Qué se puede adjuntar (como el `accept` de un input). Default: todo. */
52
+ accept?: string;
49
53
  maxFiles?: number;
50
54
  maxFileMb?: number;
55
+ /** Íconos del botón de mandar y de detener. Default: flecha hacia arriba y cuadrito. */
56
+ icons?: ComposerIcons;
51
57
  }
52
- 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, 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;
53
64
  /**
54
65
  * El selector de modelo. PORTADO de Ghosty (`ModelPicker.tsx`): dropdown discreto hacia arriba, sin
55
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";
@@ -19,6 +20,11 @@ export interface PilotoFeatures {
19
20
  duration?: boolean;
20
21
  /** Mandar con el run vivo (steer). Default: lo que diga el agente en `state.caps.steer`. */
21
22
  steer?: boolean;
23
+ /**
24
+ * Lo que el agente dice ANTES de cada herramienta como pasos con palomita («Reviso tus crons.»).
25
+ * Default: sí. `false`: todo el texto es respuesta (los pasos siguen en su caja).
26
+ */
27
+ narration?: boolean;
22
28
  }
23
29
  export interface PilotoProps extends UsePilotOptions {
24
30
  transport: PilotTransport | null;
@@ -35,6 +41,8 @@ export interface PilotoProps extends UsePilotOptions {
35
41
  welcome?: ReactNode;
36
42
  avatar?: ReactNode;
37
43
  placeholder?: string;
44
+ /** Íconos del botón redondo del composer: `send` (mandar) y `stop` (detener). Default: flecha y cuadrito. */
45
+ icons?: ComposerIcons;
38
46
  suggestions?: string[];
39
47
  features?: PilotoFeatures;
40
48
  /**
@@ -11,6 +11,8 @@ export type PilotTheme = "light" | "dark" | "auto";
11
11
  export interface MarkdownOptions {
12
12
  /** Reescribe el markdown antes de pintarlo (p. ej. `#12` → `[#12](#folio-12)`). */
13
13
  transform?: (md: string, turn?: Turn) => string;
14
+ /** Cómo se pinta un elemento del markdown, por etiqueta (`a`, `code`…). */
15
+ components?: Record<string, ComponentType<any>>;
14
16
  /** Un clic en un enlace. Devuelve `true` si la app lo manejó (no se abre el aviso de salida). */
15
17
  onLinkClick?: (href: string, event: MouseEvent) => boolean;
16
18
  }
@@ -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";
@@ -24,12 +24,12 @@ import {
24
24
  subagentsOf,
25
25
  takeQueued,
26
26
  toPermission
27
- } from "../chunks/chunk-FWBUIGTC.js";
27
+ } from "../chunks/chunk-7M2B7VKH.js";
28
28
  import {
29
29
  PilotHttpError,
30
30
  createAgUiTransport,
31
31
  readSse
32
- } from "../chunks/chunk-TMEHQZWL.js";
32
+ } from "../chunks/chunk-KG23PQI2.js";
33
33
  import {
34
34
  Activity,
35
35
  AppWindow,
@@ -70,6 +70,7 @@ import {
70
70
  Suggestions,
71
71
  TurnContext,
72
72
  UsersRound,
73
+ Wrench,
73
74
  X,
74
75
  definePlugin,
75
76
  isDev,
@@ -79,7 +80,7 @@ import {
79
80
  resolvePlugins,
80
81
  rootProps,
81
82
  usePiloto
82
- } from "../chunks/chunk-ZIUOXG7B.js";
83
+ } from "../chunks/chunk-VFCROLFF.js";
83
84
  import {
84
85
  TOOL_VERBS_EN,
85
86
  TOOL_VERBS_ES,
@@ -441,7 +442,7 @@ function Markdown({ children, animate = false, className }) {
441
442
  {
442
443
  onClickCapture: onClick,
443
444
  className: `ghosty-md min-w-0 max-w-full break-words ${className ?? "text-text-primary"} [&_pre]:max-w-full [&_pre]:overflow-x-auto [&_table]:block [&_table]:max-w-full [&_table]:overflow-x-auto`,
444
- children: animate ? /* @__PURE__ */ jsx3(ChunksContext.Provider, { value: { registry, part: 0 }, children: /* @__PURE__ */ jsx3(Streamdown, { BlockComponent: ChunkedBlock, linkSafety, children: text }) }) : /* @__PURE__ */ jsx3(Streamdown, { linkSafety, children: text })
445
+ children: animate ? /* @__PURE__ */ jsx3(ChunksContext.Provider, { value: { registry, part: 0 }, children: /* @__PURE__ */ jsx3(Streamdown, { BlockComponent: ChunkedBlock, linkSafety, components: markdown.components, children: text }) }) : /* @__PURE__ */ jsx3(Streamdown, { linkSafety, components: markdown.components, children: text })
445
446
  }
446
447
  )
447
448
  );
@@ -544,7 +545,7 @@ var DEFAULT_TOOL_ICONS = {
544
545
  };
545
546
  function iconFor(t, icons) {
546
547
  for (const name of toolNames(t)) if (icons[name]) return icons[name];
547
- return t.kind && (icons[t.kind] ?? DEFAULT_TOOL_ICONS[t.kind]) || ClawIcon;
548
+ return t.kind && (icons[t.kind] ?? DEFAULT_TOOL_ICONS[t.kind]) || icons["*"] || Wrench;
548
549
  }
549
550
  var VISIBLE = 5;
550
551
  function AgentSteps({ tools, live }) {
@@ -678,7 +679,7 @@ function UserBubble({ turn }) {
678
679
  },
679
680
  i
680
681
  )) }),
681
- (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: [
682
683
  inBubble.length > 0 && /* @__PURE__ */ jsx6("div", { className: `flex flex-wrap justify-end gap-1.5 ${visible.trim() ? "mb-2" : ""}`, children: inBubble.map(
683
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)
684
685
  ) }),
@@ -777,14 +778,22 @@ function TurnFooter({ text, seconds, onRetry, showCopy = true, showDuration = tr
777
778
  ] })
778
779
  ] });
779
780
  }
780
- function AgentBubble({ turn, live, onRetry, onOption, showUsage = true, showCopy = true, showDuration = true, renderDeliveries, renderTools, renderBefore, blocks }) {
781
+ function AgentBubble(props) {
782
+ const { turn, avatar } = props;
783
+ const body = /* @__PURE__ */ jsx6(AgentBody, { ...props, showUsage: props.showUsage ?? true, showCopy: props.showCopy ?? true, showDuration: props.showDuration ?? true });
784
+ return /* @__PURE__ */ jsx6(TurnContext.Provider, { value: turn, children: avatar ? /* @__PURE__ */ jsxs4("div", { className: "flex items-start gap-3", children: [
785
+ /* @__PURE__ */ jsx6("div", { className: "mt-0.5 shrink-0", children: avatar }),
786
+ body
787
+ ] }) : body });
788
+ }
789
+ function AgentBody({ turn, live, onRetry, onOption, showUsage, showCopy, showDuration, renderDeliveries, renderTools, renderBefore, blocks, narration = true }) {
781
790
  const { strings: t } = usePiloto();
782
791
  const extracted = blocks ? extractBlocks(turn.text ?? "", blocks.langs, turn.cuts) : { text: turn.text ?? "", cuts: turn.cuts, blocks: [] };
783
792
  const closed = !live && !turn.tools?.some((x) => x.status === "pending");
784
- const narrated = splitNarration(extracted.text, extracted.cuts, closed);
793
+ const narrated = narration ? splitNarration(extracted.text, extracted.cuts, closed) : { steps: [], rest: extracted.text };
785
794
  const visible = narrated.rest;
786
795
  const options = onOption && !live && visible ? extractReplyOptions(visible) : null;
787
- return /* @__PURE__ */ jsx6(TurnContext.Provider, { value: turn, children: /* @__PURE__ */ jsxs4("div", { className: "gc-msg-agent min-w-0 max-w-[90%]", children: [
796
+ return /* @__PURE__ */ jsxs4("div", { className: "gc-msg-agent min-w-0 max-w-[90%]", children: [
788
797
  turn.thought && /* @__PURE__ */ jsxs4("details", { className: "mb-3 text-xs text-text-secondary", children: [
789
798
  /* @__PURE__ */ jsx6("summary", { className: "cursor-pointer select-none", children: t.thinking }),
790
799
  /* @__PURE__ */ jsx6("p", { className: "mt-2 whitespace-pre-wrap border-l-2 border-border-secondary pl-3", children: turn.thought })
@@ -805,7 +814,7 @@ function AgentBubble({ turn, live, onRetry, onOption, showUsage = true, showCopy
805
814
  turn.decisions?.length ? /* @__PURE__ */ jsx6(DecisionLines, { decisions: turn.decisions }) : null,
806
815
  showUsage && turn.usage && /* @__PURE__ */ jsx6(UsageStats, { ...turn.usage }),
807
816
  turn.text && !live && /* @__PURE__ */ jsx6(TurnFooter, { text: turn.text, seconds: turn.seconds, onRetry, showCopy, showDuration })
808
- ] }) });
817
+ ] });
809
818
  }
810
819
 
811
820
  // src/react/Status.tsx
@@ -1002,7 +1011,8 @@ function usePilot(transport, opts = {}) {
1002
1011
  commit({ ...ref.current, notice: err instanceof Error && err.message ? err.message : null, effects: [] });
1003
1012
  return runNow({ messages: [] }, true);
1004
1013
  }
1005
- commit(sendFailed(ref.current, err instanceof Error && err.message ? err.message : "No se pudo hablar con el agente."));
1014
+ const failed = sendFailed(ref.current, err instanceof Error && err.message ? err.message : "No se pudo hablar con el agente.");
1015
+ commit(err instanceof PilotHttpError ? { ...failed, httpError: { status: err.status, ...err.code ? { code: err.code } : {}, message: err.message, ...err.headers ? { headers: err.headers } : {} } } : failed);
1006
1016
  return;
1007
1017
  }
1008
1018
  if (c.signal.aborted) return;
@@ -1189,6 +1199,18 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1189
1199
  useEffect5(() => {
1190
1200
  if (isDev()) for (const w of plugins.warnings) warnOnce(w);
1191
1201
  }, [plugins]);
1202
+ const ctxForComponents = useRef4(null);
1203
+ const boundComponents = useMemo3(() => {
1204
+ const entries = Object.entries(plugins.components);
1205
+ if (!entries.length) return void 0;
1206
+ return Object.fromEntries(
1207
+ entries.map(([tag, { plugin, value: C }]) => [
1208
+ tag,
1209
+ // Como toda vista de plugin: marcada con su nombre y, si es de la app, aislada de las utilidades.
1210
+ (p) => /* @__PURE__ */ jsx8("span", { ...pluginAttrs(plugin, plugins.ghostykitStyled), style: { display: "contents" }, children: /* @__PURE__ */ jsx8(C, { ...p, ctx: ctxForComponents.current }) })
1211
+ ])
1212
+ );
1213
+ }, [plugins]);
1192
1214
  const strings = useMemo3(() => ({ ...locale === "en" ? STRINGS_EN : STRINGS_ES, ...plugins.strings }), [locale, plugins]);
1193
1215
  const config = useMemo3(
1194
1216
  () => ({
@@ -1198,9 +1220,9 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1198
1220
  formatUserText: plugins.formatUserText ? (x) => plugins.formatUserText(cleanUserText(x)) : cleanUserText,
1199
1221
  theme,
1200
1222
  skin: props.skin ?? "ghosty",
1201
- markdown: plugins.markdown
1223
+ markdown: { ...plugins.markdown, ...boundComponents ? { components: boundComponents } : {} }
1202
1224
  }),
1203
- [strings, locale, plugins, theme, props.skin]
1225
+ [strings, locale, plugins, theme, props.skin, boundComponents]
1204
1226
  );
1205
1227
  const t = strings;
1206
1228
  const [ownRefs, setOwnRefs] = useState8([]);
@@ -1235,12 +1257,16 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1235
1257
  const send = useCallback2(
1236
1258
  (text, attachments = []) => {
1237
1259
  const current = refsRef.current;
1238
- const full = applyReferences(text, current);
1260
+ const before = pluginsRef.current.beforeSend;
1261
+ const full = applyReferences(before && ctxRef.current ? before(text, ctxRef.current) : text, current);
1239
1262
  if (current.length) setRefs([]);
1240
1263
  void pilot.send(full, attachments, current.length ? { references: current } : {});
1241
1264
  },
1242
1265
  [pilot, setRefs]
1243
1266
  );
1267
+ const pluginsRef = useRef4(plugins);
1268
+ pluginsRef.current = plugins;
1269
+ const ctxRef = useRef4(null);
1244
1270
  const composerHandle = useRef4(null);
1245
1271
  const focus = useCallback2(() => composerBox.current?.querySelector("textarea")?.focus({ preventScroll: true }), []);
1246
1272
  const addReference = useCallback2(
@@ -1256,10 +1282,9 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1256
1282
  const actions = { pilot, send, addReference, addAttachments, setDraft, focus };
1257
1283
  useImperativeHandle(handle, () => actions, [pilot, send, addReference, addAttachments, setDraft, focus]);
1258
1284
  const ctx = { ...actions, agentState: state.agentState, strings, plugins: plugins.list };
1259
- const ctxRef = useRef4(ctx);
1285
+ ctxForComponents.current = ctx;
1286
+ const mark = (plugin) => pluginAttrs(plugin, plugins.ghostykitStyled);
1260
1287
  ctxRef.current = ctx;
1261
- const pluginsRef = useRef4(plugins);
1262
- pluginsRef.current = plugins;
1263
1288
  const unattended = useRef4(/* @__PURE__ */ new Set());
1264
1289
  useEffect5(
1265
1290
  () => pilot.subscribe?.((event) => {
@@ -1315,6 +1340,17 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1315
1340
  }),
1316
1341
  [pilot.onTurn]
1317
1342
  );
1343
+ useEffect5(() => {
1344
+ const err = state.httpError;
1345
+ if (!err) return;
1346
+ for (const h of pluginsRef.current.httpError) {
1347
+ try {
1348
+ h.value(err, ctxRef.current);
1349
+ } catch (e) {
1350
+ console.error(`[ghostykit] el plugin \xAB${h.plugin}\xBB fall\xF3 con onHttpError`, e);
1351
+ }
1352
+ }
1353
+ }, [state.httpError]);
1318
1354
  const [held, setHeld] = useState8({});
1319
1355
  const heldRef = useRef4(held);
1320
1356
  heldRef.current = held;
@@ -1428,12 +1464,20 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1428
1464
  const toolRunning = busy && last?.role === "assistant" && (last.tools ?? []).some(isRunning);
1429
1465
  const empty = turns.length === 0 && !busy;
1430
1466
  const placeholder = props.placeholder ?? (props.agentName ? t.placeholder(props.agentName) : t.placeholderDefault);
1431
- const { upload, transcribe, voiceNoteText } = plugins.attachments;
1467
+ const { upload, transcribe, voiceNoteText, accept, maxFiles, maxBytes } = plugins.attachments;
1432
1468
  const showFiles = features.attachments ?? Boolean(upload);
1433
1469
  const pointedRefs = refs.filter((r) => r.prompt);
1434
1470
  const plainRefs = refs.filter((r) => !r.prompt);
1435
1471
  const removeRef = (id) => setRefs(refsRef.current.filter((r) => r.id !== id));
1436
- const composer = /* @__PURE__ */ jsxs6("div", { ref: composerBox, children: [
1472
+ const replacer = plugins.replacers.find((r) => {
1473
+ try {
1474
+ return r.value.when(ctx);
1475
+ } catch {
1476
+ return false;
1477
+ }
1478
+ });
1479
+ const Replacement = replacer?.value.view;
1480
+ const composer = Replacement ? /* @__PURE__ */ jsx8("div", { ...mark(replacer.plugin), children: /* @__PURE__ */ jsx8(Replacement, { ctx }) }) : /* @__PURE__ */ jsxs6("div", { ref: composerBox, children: [
1437
1481
  /* @__PURE__ */ jsx8(ReferenceChips, { references: pointedRefs, onRemove: removeRef, onActivate: props.onReferenceClick }),
1438
1482
  /* @__PURE__ */ jsx8(
1439
1483
  Composer,
@@ -1453,7 +1497,11 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1453
1497
  onActivateReference: props.onReferenceClick,
1454
1498
  extras: plugins.composerActions.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.composerActions, ctx, styled: plugins.ghostykitStyled, inline: true }) : void 0,
1455
1499
  extrasEnd: plugins.composerActionsEnd.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.composerActionsEnd, ctx, styled: plugins.ghostykitStyled, inline: true }) : void 0,
1456
- handleRef: composerHandle
1500
+ handleRef: composerHandle,
1501
+ icons: props.icons,
1502
+ accept,
1503
+ ...maxFiles ? { maxFiles } : {},
1504
+ ...maxBytes ? { maxFileMb: maxBytes / (1024 * 1024) } : {}
1457
1505
  },
1458
1506
  draft.n
1459
1507
  )
@@ -1464,8 +1512,12 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1464
1512
  ] }) : chat;
1465
1513
  const aboveComposer = plugins.composerAbove.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.composerAbove, ctx, styled: plugins.ghostykitStyled }) : null;
1466
1514
  const belowComposer = plugins.composerBelow.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.composerBelow, ctx, styled: plugins.ghostykitStyled }) : null;
1467
- const header = plugins.header.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.header, ctx, styled: plugins.ghostykitStyled, inline: true }) : null;
1468
- const mark = (plugin) => pluginAttrs(plugin, plugins.ghostykitStyled);
1515
+ const headerStart = plugins.headerStart.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.headerStart, ctx, styled: plugins.ghostykitStyled, inline: true }) : null;
1516
+ const headerEnd = plugins.header.length ? /* @__PURE__ */ jsx8(Views, { items: plugins.header, ctx, styled: plugins.ghostykitStyled, inline: true }) : null;
1517
+ const headerSides = (headerStart || headerEnd) && /* @__PURE__ */ jsxs6(Fragment4, { children: [
1518
+ headerStart && /* @__PURE__ */ jsx8("div", { className: "piloto-header-start flex min-w-0 items-center gap-2", children: headerStart }),
1519
+ headerEnd && /* @__PURE__ */ jsx8("div", { className: "piloto-header-end ml-auto flex items-center gap-2", children: headerEnd })
1520
+ ] });
1469
1521
  const Empty = plugins.empty?.value;
1470
1522
  const Waiting = plugins.waiting?.value;
1471
1523
  const renderDeliveries = (turn, live) => {
@@ -1516,7 +1568,7 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1516
1568
  // Un hilo vacío enseña lo mismo que la portada: la cara, el saludo, la caja y por dónde
1517
1569
  // empezar. Si no cabe (panel bajo), se desplaza: `safe center` nunca empuja nada fuera.
1518
1570
  /* @__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: [
1519
- header && /* @__PURE__ */ jsx8("div", { className: "absolute right-4 top-3 flex gap-2", children: header }),
1571
+ 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 }),
1520
1572
  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: [
1521
1573
  props.avatar && /* @__PURE__ */ jsx8("div", { className: "mb-3", children: props.avatar }),
1522
1574
  props.welcome && // Un saludo largo (una explicación del piloto) baja a letra de lectura.
@@ -1536,7 +1588,7 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1536
1588
  ] })
1537
1589
  ] })
1538
1590
  ) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
1539
- header && /* @__PURE__ */ jsx8("div", { className: "flex shrink-0 justify-end gap-2 px-4 pt-3 sm:px-6", children: header }),
1591
+ headerSides && /* @__PURE__ */ jsx8("div", { className: "piloto-header flex shrink-0 items-center gap-2 px-4 pt-3 sm:px-6", children: headerSides }),
1540
1592
  /* @__PURE__ */ jsxs6(
1541
1593
  "div",
1542
1594
  {
@@ -1585,7 +1637,9 @@ var Piloto = forwardRef(function Piloto2(props, handle) {
1585
1637
  renderDeliveries,
1586
1638
  renderTools,
1587
1639
  renderBefore,
1588
- blocks
1640
+ blocks,
1641
+ narration: features.narration !== false,
1642
+ avatar: plugins.avatar ? /* @__PURE__ */ jsx8("span", { ...mark(plugins.avatar.plugin), children: /* @__PURE__ */ jsx8(plugins.avatar.value, { ctx, turn }) }) : void 0
1589
1643
  }
1590
1644
  ),
1591
1645
  turn.role === "assistant" && /* @__PURE__ */ jsx8(TurnNotes, { turn })
@@ -2,7 +2,7 @@ import type { ComponentType, ReactNode } from "react";
2
2
  import type { BaseEvent } from "@ag-ui/core";
3
3
  import type { TextBlock } from "../core/narration";
4
4
  import type { ToolVerbs } from "../core/steps";
5
- import type { Attachment, CustomEntry, Reference, ToolEntry, Turn } from "../core/types";
5
+ import type { Attachment, CustomEntry, PilotState, Reference, ToolEntry, Turn } from "../core/types";
6
6
  import type { UploadResult } from "./Composer";
7
7
  import type { MarkdownOptions, PilotIcon } from "./context";
8
8
  import type { PilotStrings } from "./strings";
@@ -52,7 +52,17 @@ export interface PluginMarkdown {
52
52
  turn: Turn;
53
53
  live: boolean;
54
54
  }>>;
55
+ /**
56
+ * Cómo se pinta un elemento del markdown, por etiqueta (`a`, `code`, `img`…): p. ej. un enlace que
57
+ * es un CTA como botón. Recibe las props del elemento y `ctx`. Por etiqueta, gana el primer plugin.
58
+ */
59
+ components?: Record<string, ComponentType<Record<string, unknown> & {
60
+ ctx: PluginCtx;
61
+ children?: ReactNode;
62
+ }>>;
55
63
  }
64
+ /** Un rechazo HTTP del servidor antes del stream (403 con `x-needs-login`, 429…). */
65
+ export type PluginHttpError = NonNullable<PilotState["httpError"]>;
56
66
  export interface PilotoPlugin {
57
67
  /** Único: aparece en `data-plugin`, en los avisos y en `pilotDevtools()`. */
58
68
  name: string;
@@ -115,6 +125,12 @@ export interface PilotoPlugin {
115
125
  }>>;
116
126
  /** El texto del usuario como se pinta (se encadena en orden). */
117
127
  formatUserText?: (text: string) => string;
128
+ /** El texto que sale, antes de mandarlo (p. ej. anteponer lo señalado). Se encadena en orden. */
129
+ beforeSend?: (text: string, ctx: PluginCtx) => string;
130
+ /** El avatar del agente junto a cada respuesta. Gana el primer plugin. */
131
+ avatar?: PluginView<{
132
+ turn: Turn;
133
+ }>;
118
134
  };
119
135
  composer?: {
120
136
  /** Botones en la barra del composer, entre la imagen y el clip («Mis archivos», «Señalar»). */
@@ -130,10 +146,27 @@ export interface PilotoPlugin {
130
146
  upload?: (file: File) => Promise<UploadResult>;
131
147
  transcribe?: (audio: Blob) => Promise<string>;
132
148
  voiceNoteText?: (transcript: string) => string;
149
+ /** Qué se puede adjuntar (como el `accept` de un input): `"image/*"` = sólo fotos. */
150
+ accept?: string;
151
+ /** Cuántos archivos a la vez (default 8). */
152
+ maxFiles?: number;
153
+ /** Tamaño máximo por archivo, en bytes (default 25 MB). */
154
+ maxBytes?: number;
155
+ };
156
+ /**
157
+ * En lugar de la caja, cuando `when(ctx)` es verdad (p. ej. la prueba se acabó: 403 con
158
+ * `x-needs-login` → «Crea tu cuenta»). Gana el primer plugin cuyo `when` es verdad.
159
+ */
160
+ replace?: {
161
+ when: (ctx: PluginCtx) => boolean;
162
+ view: PluginView;
133
163
  };
134
164
  };
135
- /** Arriba a la derecha, sobre el hilo. */
136
- 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;
137
170
  /** Junto al chat. */
138
171
  sidePanel?: PluginView;
139
172
  /** Arriba de los mensajes, dentro del hilo. */
@@ -150,11 +183,20 @@ export interface PilotoPlugin {
150
183
  ctx: PluginCtx;
151
184
  children: ReactNode;
152
185
  }>;
186
+ /** El servidor rechazó un mensaje antes de contestar (`ctx.pilot.state.httpError` también lo guarda). */
187
+ onHttpError?: (error: PluginHttpError, ctx: PluginCtx) => void;
153
188
  /** Este cliente mandó un turno. */
154
189
  onTurnStart?: (ctx: PluginCtx) => void;
155
190
  /** Terminó un turno de este cliente (abrir la última entrega, re-leer datos). */
156
191
  onTurnDone?: (info: TurnDoneInfo, ctx: PluginCtx) => void;
157
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
+ }
158
200
  /** Ayuda de tipos: `definePlugin({ name: "crm", … })`. */
159
201
  export declare function definePlugin<T extends PilotoPlugin>(plugin: T): T;
160
202
  type Owned<T> = {
@@ -174,10 +216,18 @@ export interface ResolvedPlugins {
174
216
  toolRenders: Record<string, Owned<NonNullable<NonNullable<PilotoPlugin["tools"]>["render"]>[string]>>;
175
217
  customRenders: Record<string, Owned<NonNullable<NonNullable<PilotoPlugin["message"]>["custom"]>[string]>>;
176
218
  blockRenders: Record<string, Owned<NonNullable<PluginMarkdown["blocks"]>[string]>>;
219
+ components: Record<string, Owned<NonNullable<PluginMarkdown["components"]>[string]>>;
220
+ avatar?: Owned<NonNullable<NonNullable<PilotoPlugin["message"]>["avatar"]>>;
221
+ beforeSend?: (text: string, ctx: PluginCtx) => string;
222
+ replacers: Owned<NonNullable<NonNullable<PilotoPlugin["composer"]>["replace"]>>[];
223
+ httpError: Owned<NonNullable<PilotoPlugin["onHttpError"]>>[];
177
224
  attachments: NonNullable<NonNullable<PilotoPlugin["composer"]>["attachments"]>;
178
225
  empty?: Owned<PluginView>;
179
226
  waiting?: Owned<NonNullable<PilotoPlugin["waiting"]>>;
227
+ /** La barra de arriba, a la derecha (`header` o `header.end`). */
180
228
  header: Owned<PluginView>[];
229
+ /** La barra de arriba, a la izquierda (`header.start`). */
230
+ headerStart: Owned<PluginView>[];
181
231
  sidePanel: Owned<PluginView>[];
182
232
  aboveThread: Owned<PluginView>[];
183
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 ──────────
@@ -13,7 +13,9 @@ export declare class PilotHttpError extends Error {
13
13
  code?: string;
14
14
  /** Segundos de `Retry-After` (429/503). */
15
15
  retryAfter?: number;
16
- constructor(message: string, status: number, code?: string, retryAfter?: number);
16
+ /** Las cabeceras de la respuesta (minúsculas), p. ej. `x-needs-login`. */
17
+ headers?: Record<string, string>;
18
+ constructor(message: string, status: number, code?: string, retryAfter?: number, headers?: Record<string, string>);
17
19
  }
18
20
  export interface AgUiTransportOptions {
19
21
  /** El endpoint del agente: `POST <url>`. */
@@ -2,7 +2,7 @@ import {
2
2
  PilotHttpError,
3
3
  createAgUiTransport,
4
4
  readSse
5
- } from "../chunks/chunk-TMEHQZWL.js";
5
+ } from "../chunks/chunk-KG23PQI2.js";
6
6
  export {
7
7
  PilotHttpError,
8
8
  createAgUiTransport,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ghostykit",
3
- "version": "0.7.0",
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.0",
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",