ghostykit 0.6.4 → 0.7.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
@@ -97,34 +97,115 @@ const ref = useRef<PilotoHandle>(null);
97
97
  draft={{ text: "Why did the monitor fail?", key: row.id }} // prefilled + focused, not sent
98
98
  threadId={conversationId} // reopen a stored thread
99
99
  context={() => ({ screen: "crons", selected })} // AG-UI `context`, every run
100
- toolVerbs={{ run_cron: { one: "Ran a cron", many: "Ran %n crons", running: "Running the cron…" } }}
101
- upload={async (file) => ({ url: await myUpload(file) })}
102
- transcribe={async (audio) => myWhisper(audio)}
103
100
  onTurnDone={({ text }) => refreshMyData()}
104
- slots={{ renderDeliveries: (turn) => <MyFiles turn={turn} />, composerExtras: <MyButton /> }}
101
+ plugins={PLUGINS} // everything else: see Customize
105
102
  />;
106
103
 
107
104
  ref.current?.addReference({ id: "12", label: "#12 Rosa" }); // "@" a row into the message
108
105
  ref.current?.send("Why did this fail?"); // ask on the user's behalf
109
106
  ```
110
107
 
111
- Other options: `strings`, `toolIcons`, `formatUserText`, `markdown`, `features`
112
- (`attachments`, `retry`, `copy`, `usage`, `replyOptions`, `duration`, `steer`), `references` /
113
- `onReferencesChange`, `voiceNoteText`, `placeholder`, `avatar`, `initialTurns`, `loadOlder` (earlier
114
- pages of a long thread), `forwardedProps` (extra keys for every run, e.g. `{permissions: "ask"}`), the
115
- slots `sidePanel` and `aboveComposer` (both may take the `pilot`), and the callbacks `onTurnStart`,
116
- `onTitle`, `onState`, `onEvent` (every raw AG-UI event), `onSend`. The agent's shared state
117
- (`STATE_SNAPSHOT` + `STATE_DELTA`) is `pilot.state.agentState`, and each turn's `CUSTOM` events are
118
- `turn.custom` (what `renderDeliveries` draws).
108
+ Other props: `features` (`attachments`, `retry`, `copy`, `usage`, `replyOptions`, `duration`,
109
+ `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
+ (earlier pages of a long thread), `forwardedProps` (extra keys for every run, e.g.
112
+ `{permissions: "ask"}`), and the callbacks `onTurnStart`, `onTitle`, `onState`, `onEvent` (every raw
113
+ AG-UI event), `onSend`. The agent's shared state (`STATE_SNAPSHOT` + `STATE_DELTA`) is
114
+ `pilot.state.agentState`, and each turn's `CUSTOM` events are `turn.custom`.
119
115
 
120
116
  Subagents (`state.subagents.tasks`) are `pilot.subagents`; `pilot.stopSubagent(id)` stops one, and
121
- `<Subagents pilot={pilot} />` is a ready-made bar ("2 agents working", one row each, *Stop*):
122
- `slots={{ aboveComposer: (pilot) => <Subagents pilot={pilot} /> }}`. A message the server can't take
123
- while a run is live (`409 run_active`) waits in `pilot.state.queued` and is sent when that run ends.
124
- `pilot.refresh()` asks for the thread again (when your own event bus says something changed). To read the state outside the chat, call `usePilot(transport)` yourself
125
- and pass the result as `pilot`. Every piece is
126
- exported (`AgentBubble`, `AgentSteps`, `Composer`, `ConfirmCard`, `Markdown`, …) if you want to build
127
- your own layout.
117
+ `<Subagents pilot={pilot} />` is a ready-made bar ("2 agents working", one row each, *Stop*). A
118
+ message the server can't take while a run is live (`409 run_active`) waits in `pilot.state.queued`
119
+ and is sent when that run ends. `pilot.refresh()` asks for the thread again (when your own event bus
120
+ says something changed). To read the state outside the chat, call `usePilot(transport)` yourself and
121
+ pass the result as `pilot`. Every piece is exported (`AgentBubble`, `AgentSteps`, `Composer`,
122
+ `ConfirmCard`, `Markdown`, …) if you want to build your own layout.
123
+
124
+ ## Customize
125
+
126
+ There is **one** way to customize `<Piloto>`: **plugins**. A plugin is an object with a `name` and
127
+ named extension points. Everything a plugin draws is a React **component**: it is mounted as
128
+ `<View ctx={…} />`, so it can use hooks. Branding is the skin plus `--piloto-*` tokens.
129
+
130
+ ```tsx
131
+ import { Piloto, definePlugin } from "ghostykit";
132
+ import { livePage, modelPicker, pilotDevtools } from "ghostykit/plugins";
133
+
134
+ const crm = definePlugin({
135
+ name: "crm",
136
+ requires: { events: ["crm.card"] }, // what it handles, visible at a glance
137
+ tools: { verbs: { find_lead: { one: "Found the lead", many: "Found %n leads", running: "Looking…" } } },
138
+ message: { custom: { "crm.card": ({ entry, ctx }) => <LeadCard lead={entry.value} onOpen={() => ctx.send("Open it")} /> } },
139
+ composer: { actions: ({ ctx }) => <PointButton onPick={(row) => ctx.addReference(row)} /> },
140
+ });
141
+
142
+ // Create them ONCE (module scope or useMemo): some plugins keep state.
143
+ const PLUGINS = [crm, modelPicker({ onChange: (model) => api.setModel(model) }), livePage(), pilotDevtools()];
144
+
145
+ <Piloto transport={transport} skin="ghosty" plugins={PLUGINS} />;
146
+ ```
147
+
148
+ | Point | What it does | Example |
149
+ |---|---|---|
150
+ | `name` | Unique. Marks everything it draws with `data-plugin="<name>"`. | `"crm"` |
151
+ | `styles` | Whose styles its views use. `"host"` (default): your app's; ghostykit's utilities stay out (`data-piloto-isolate`), so they never override your classes. `"ghostykit"`: for views built from ghostykit components (`<Subagents>`, `ModelPicker`). | `"ghostykit"` |
152
+ | `requires` | `{events, state}` it handles. Derived from `events`/`state`/`message.custom` if missing; in development a mismatch is a warning. | `{ events: ["ghosty.page"] }` |
153
+ | `strings` | Texts over the locale's. | `{ stop: "Para" }` |
154
+ | `markdown.transform` | Rewrites the answer's markdown, with the turn at hand (chained). | `(md, turn) => md.replace(/#(\d+)/g, "[#$1](#folio-$1)")` |
155
+ | `markdown.onLinkClick` | A click on a link; `true` if handled (asked in order). | open in-app |
156
+ | `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 }) => … }` |
157
+ | `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 }` |
158
+ | `vars`, `className` | Branding on the root: `--piloto-*` tokens and classes. | `{ "--piloto-radius-bubble": "6px" }` |
159
+ | `tools.verbs` | How each tool is named in the steps. | `{ run_cron: { one, many, running } }` |
160
+ | `tools.icons` | Icon by tool name or `kind`. | `{ run_cron: Clock }` |
161
+ | `tools.render` | A tool's own view, under the steps, by tool name or `kind`. | `{ create_image: ImagePreview }` |
162
+ | `events` | Handlers by AG-UI type, by `CUSTOM` name, or `*`. | `{ "ghosty.ui": (e, ctx) => board.open(e.value) }` |
163
+ | `state` | Called when that key of `agentState` changes. | `{ model: (v, ctx) => … }` |
164
+ | `message.custom` | Draws a `turn.custom` entry by its name (deliveries, CRM cards). | `{ "ghosty.delivery": DeliveryCard }` |
165
+ | `message.assistantBefore` | Before each answer's text, under its steps. | live page progress, "Creating image…" |
166
+ | `message.user` / `message.assistant` | Extras under each user message / answer. | `({ turn, live }) => …` |
167
+ | `message.formatUserText` | How the user's text is shown (chained). | strip app plumbing |
168
+ | `message.beforeSend` | Rewrites the text that goes out (chained). | prepend what was pointed at |
169
+ | `message.avatar` | The agent's avatar next to each answer. | `() => <img src={logo} />` |
170
+ | `composer.actions` | Buttons in the composer bar, between the image button and the clip. | "My files", "Point" |
171
+ | `composer.actionsEnd` | Buttons after the clip. | model picker |
172
+ | `composer.above` | Above the message box. | `<Subagents pilot={ctx.pilot} />` |
173
+ | `composer.below` | Under the message box. | suggestion chips |
174
+ | `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
+ | `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 |
177
+ | `sidePanel` | Next to the chat. | live page, subagents |
178
+ | `aboveThread` | Above the messages. | a banner |
179
+ | `empty` | The empty thread (instead of the greeting). | onboarding |
180
+ | `waiting` | While the agent hasn't answered (instead of the waiting phrase). Gets `step`, `since`. | "Creating your image…" |
181
+ | `wrap` | A provider around the whole chat (`{ctx, children}`); the first plugin is outermost. | artifacts panel |
182
+ | `onHttpError` | The server refused a message before answering (`{status, code, message, headers}`; also in `pilot.state.httpError` until the next send). | 429 → upgrade banner |
183
+ | `onTurnStart` / `onTurnDone` | This client started / finished a turn. | open the last delivery |
184
+
185
+ Every view gets `ctx`: `{pilot, agentState, strings, send, addReference, addAttachments, setDraft, focus, plugins}`.
186
+ `addAttachments` takes `File`s (they go through `upload`) or ready `Attachment`s (e.g. from "My files"),
187
+ and they can be sent without text. The `ref` of `<Piloto>` is the same handle (without `plugins`,
188
+ `strings`, `agentState`), so the app never needs a separate `usePilot`: `ref.current.addReference(…)`,
189
+ `ref.current.pilot.reconnect()` (when your own event bus says the thread changed),
190
+ `ref.current.pilot.stop()`. `initialPrompt` sends a message as soon as the thread is open (a question
191
+ in the URL).
192
+
193
+ **Order and precedence**, in the order of the `plugins` list:
194
+
195
+ - **Maps** (`strings`, `tools.verbs`, `tools.icons`, `vars`) are merged, and later plugins win. `markdown.transform`, `formatUserText` and `beforeSend` are chained.
196
+ - **`events` and `state`** call every handler, in order.
197
+ - **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.
199
+
200
+ In development, ghostykit also warns once about a `CUSTOM` event that nothing handles.
201
+
202
+ **Ready-made plugins** (`ghostykit/plugins`), usable as they are and as templates:
203
+
204
+ | Plugin | What it does |
205
+ |---|---|
206
+ | `modelPicker({ onChange, stateKey? })` | The model picker in the composer bar, from `agentState.model = {current, options}`. Switching is your API (`onChange`). |
207
+ | `livePage({ width? })` | A side panel that follows `CUSTOM ghosty.page` (plan, sections, ready, error) with the link at the end. |
208
+ | `pilotDevtools({ events? })` | Prints the mounted plugins, their points and what they handle; with `events: true`, every event and who handles it. |
128
209
 
129
210
  ### Skins and theming
130
211
 
@@ -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: [] };
@@ -540,7 +540,7 @@ function isNarration(chunk) {
540
540
  const t = chunk.trim();
541
541
  return t.length > 0 && t.length <= 300 && !/\n\s*\n/.test(t) && !t.includes("```");
542
542
  }
543
- function splitNarration(text, cuts) {
543
+ function splitNarration(text, cuts, closed = false) {
544
544
  if (cuts?.length) {
545
545
  const valid = cuts.filter((c) => c > 0 && c <= text.length);
546
546
  if (valid.length) {
@@ -552,7 +552,9 @@ function splitNarration(text, cuts) {
552
552
  from = c;
553
553
  }
554
554
  if (chunks.every(isNarration)) {
555
- return { steps: chunks.map((t) => t.replace(/\s*\n\s*/g, " ")), rest: text.slice(from).replace(/^\s+/, "") };
555
+ const rest = text.slice(from).replace(/^\s+/, "");
556
+ if (closed && !rest.trim()) return { steps: chunks.slice(0, -1).map((t) => t.replace(/\s*\n\s*/g, " ")), rest: chunks[chunks.length - 1] };
557
+ return { steps: chunks.map((t) => t.replace(/\s*\n\s*/g, " ")), rest };
556
558
  }
557
559
  }
558
560
  return { steps: [], rest: text };
@@ -576,6 +578,30 @@ function narrationToMarkdown(chunks, answer) {
576
578
 
577
579
  ${answer.trim()}` : list;
578
580
  }
581
+ function extractBlocks(text, langs, cuts) {
582
+ if (!langs.length || !text.includes("```")) return { text, cuts, blocks: [] };
583
+ const want = new Set(langs);
584
+ const blocks = [];
585
+ const removed = [];
586
+ const re = /(^|\n)```([\w.-]+)[^\n]*\n([\s\S]*?)(\n```[ \t]*(?=\n|$)|$)/g;
587
+ for (let m = re.exec(text); m; m = re.exec(text)) {
588
+ if (!want.has(m[2])) continue;
589
+ const start = m.index + m[1].length;
590
+ const end = m.index + m[0].length;
591
+ blocks.push({ lang: m[2], body: m[3], complete: Boolean(m[4]) });
592
+ removed.push([start, end]);
593
+ }
594
+ if (!removed.length) return { text, cuts, blocks };
595
+ let out = "";
596
+ let from = 0;
597
+ for (const [a, b] of removed) {
598
+ out += text.slice(from, a);
599
+ from = b;
600
+ }
601
+ out += text.slice(from);
602
+ const shift = (c) => c - removed.reduce((n, [a, b]) => n + (c >= b ? b - a : c > a ? c - a : 0), 0);
603
+ return { text: out, ...cuts ? { cuts: cuts.map(shift) } : {}, blocks };
604
+ }
579
605
 
580
606
  // src/core/options.ts
581
607
  var BULLET = /^\s*(?:[-*•+]|(\d{1,2})[.)])\s+(.+?)\s*$/;
@@ -606,140 +632,6 @@ function cleanReplyOption(t) {
606
632
  return s.charAt(0).toLocaleUpperCase() + s.slice(1);
607
633
  }
608
634
 
609
- // src/core/steps.ts
610
- var isRunning = (t) => t.status !== "completed" && t.status !== "failed" && t.status !== "cancelled";
611
- function toolNames(t) {
612
- const title = t.title ?? "";
613
- if (!title) return [];
614
- const out = [title];
615
- const double = /^mcp__.+?__(.+)$/i.exec(title);
616
- if (double) out.push(double[1]);
617
- else if (/^mcp_/i.test(title)) {
618
- const parts = title.slice(4).split("_");
619
- for (let i = 0; i < parts.length; i++) out.push(parts.slice(i).join("_"));
620
- }
621
- return [...new Set(out)];
622
- }
623
- function toolName(t) {
624
- const names = toolNames(t);
625
- return /^mcp__/i.test(names[0] ?? "") ? names[1] ?? names[0] : names[0] ?? null;
626
- }
627
- function namedVerb(t, verbs) {
628
- for (const name of toolNames(t)) if (verbs[name]) return { key: name, verb: verbs[name] };
629
- return null;
630
- }
631
- function stepLabel(t, verbs, fallback = "Paso") {
632
- if (t.label) return t.label;
633
- const named = namedVerb(t, verbs);
634
- if (named) return isRunning(t) && named.verb.running ? named.verb.running : named.verb.one;
635
- return t.title ?? t.kind ?? fallback;
636
- }
637
- function summarizeTools(tools, verbs) {
638
- const groups = /* @__PURE__ */ new Map();
639
- for (const t of tools) {
640
- const named = namedVerb(t, verbs);
641
- const key = named?.key ?? t.kind ?? "other";
642
- const g = groups.get(key) ?? { n: 0, verb: named?.verb ?? verbs[key] };
643
- g.n++;
644
- groups.set(key, g);
645
- }
646
- const parts = [];
647
- for (const [key, { n, verb }] of groups) {
648
- if (verb) parts.push(n === 1 ? verb.one : verb.many.replace("%n", String(n)));
649
- else parts.push(`${n} \xD7 ${key.replace(/[_-]/g, " ")}`);
650
- }
651
- return parts.join(" \xB7 ");
652
- }
653
- function groupSteps(tools, label) {
654
- const groups = /* @__PURE__ */ new Map();
655
- for (const t of tools) {
656
- const k = `${label(t)}\0${t.detail ?? ""}`;
657
- const g = groups.get(k);
658
- if (!g) {
659
- groups.set(k, { tool: t, times: 1 });
660
- continue;
661
- }
662
- g.times++;
663
- const wins = isRunning(t) || t.status === "failed" || !(isRunning(g.tool) || g.tool.status === "failed");
664
- if (wins) g.tool = { ...t, id: g.tool.id };
665
- }
666
- return [...groups.values()];
667
- }
668
- var TOOL_VERBS_ES = {
669
- read: { one: "Le\xED un archivo", many: "Le\xED %n archivos" },
670
- read_file: { one: "Le\xED un archivo", many: "Le\xED %n archivos" },
671
- write: { one: "Escrib\xED un archivo", many: "Escrib\xED %n archivos" },
672
- write_file: { one: "Escrib\xED un archivo", many: "Escrib\xED %n archivos" },
673
- edit: { one: "Edit\xE9 un archivo", many: "Edit\xE9 %n archivos" },
674
- edit_file: { one: "Edit\xE9 un archivo", many: "Edit\xE9 %n archivos" },
675
- bash: { one: "Corr\xED un comando", many: "Corr\xED %n comandos" },
676
- shell: { one: "Corr\xED un comando", many: "Corr\xED %n comandos" },
677
- terminal: { one: "Corr\xED un comando", many: "Corr\xED %n comandos" },
678
- execute: { one: "Corr\xED un comando", many: "Corr\xED %n comandos" },
679
- grep: { one: "Busqu\xE9 en el c\xF3digo", many: "Busqu\xE9 en el c\xF3digo" },
680
- glob: { one: "Busqu\xE9 archivos", many: "Busqu\xE9 archivos" },
681
- search: { one: "Busqu\xE9", many: "Busqu\xE9 %n veces" },
682
- tool_search: { one: "Busqu\xE9 una herramienta", many: "Busqu\xE9 %n herramientas" },
683
- web_search: { one: "Busqu\xE9 en la web", many: "Busqu\xE9 en la web" },
684
- web_fetch: { one: "Abr\xED una p\xE1gina", many: "Abr\xED %n p\xE1ginas" },
685
- fetch: { one: "Abr\xED una p\xE1gina", many: "Abr\xED %n p\xE1ginas" },
686
- site_publish: { one: "Publiqu\xE9 una p\xE1gina", many: "Publiqu\xE9 %n p\xE1ginas" },
687
- pdf: { one: "Gener\xE9 un PDF", many: "Gener\xE9 %n PDF" },
688
- connector: { one: "Us\xE9 tu sistema", many: "Us\xE9 tu sistema %n veces" },
689
- mcp: { one: "Us\xE9 una herramienta", many: "Us\xE9 %n herramientas" },
690
- browser: { one: "Us\xE9 el navegador", many: "Us\xE9 el navegador" },
691
- todo: { one: "Anot\xE9 el plan", many: "Anot\xE9 el plan" },
692
- delete: { one: "Borr\xE9 un archivo", many: "Borr\xE9 %n archivos" },
693
- move: { one: "Mov\xED un archivo", many: "Mov\xED %n archivos" },
694
- think: { one: "Lo pens\xE9", many: "Lo pens\xE9" },
695
- image: { one: "Cre\xE9 una imagen", many: "Cre\xE9 %n im\xE1genes" },
696
- delegate: { one: "Delegu\xE9 una tarea", many: "Delegu\xE9 %n tareas" },
697
- collect: { one: "Recog\xED un resultado", many: "Recog\xED %n resultados" },
698
- status: { one: "Pregunt\xE9 c\xF3mo iba", many: "Pregunt\xE9 %n veces c\xF3mo iba" },
699
- stop: { one: "Detuve una tarea", many: "Detuve %n tareas" },
700
- guide: { one: "Le\xED una gu\xEDa", many: "Le\xED %n gu\xEDas" },
701
- deliver: { one: "Entregu\xE9 un archivo", many: "Entregu\xE9 %n archivos" },
702
- sheet: { one: "Consult\xE9 una hoja", many: "Consult\xE9 %n hojas" },
703
- view: { one: "Arm\xE9 una vista", many: "Arm\xE9 %n vistas" }
704
- };
705
- var TOOL_VERBS_EN = {
706
- read: { one: "Read a file", many: "Read %n files" },
707
- read_file: { one: "Read a file", many: "Read %n files" },
708
- write: { one: "Wrote a file", many: "Wrote %n files" },
709
- write_file: { one: "Wrote a file", many: "Wrote %n files" },
710
- edit: { one: "Edited a file", many: "Edited %n files" },
711
- edit_file: { one: "Edited a file", many: "Edited %n files" },
712
- bash: { one: "Ran a command", many: "Ran %n commands" },
713
- shell: { one: "Ran a command", many: "Ran %n commands" },
714
- terminal: { one: "Ran a command", many: "Ran %n commands" },
715
- execute: { one: "Ran a command", many: "Ran %n commands" },
716
- grep: { one: "Searched the code", many: "Searched the code" },
717
- glob: { one: "Searched for files", many: "Searched for files" },
718
- search: { one: "Searched", many: "Searched %n times" },
719
- tool_search: { one: "Looked for a tool", many: "Looked for %n tools" },
720
- web_search: { one: "Searched the web", many: "Searched the web" },
721
- web_fetch: { one: "Opened a page", many: "Opened %n pages" },
722
- fetch: { one: "Opened a page", many: "Opened %n pages" },
723
- site_publish: { one: "Published a page", many: "Published %n pages" },
724
- pdf: { one: "Generated a PDF", many: "Generated %n PDFs" },
725
- connector: { one: "Used your system", many: "Used your system %n times" },
726
- mcp: { one: "Used a tool", many: "Used %n tools" },
727
- browser: { one: "Used the browser", many: "Used the browser" },
728
- todo: { one: "Wrote down the plan", many: "Wrote down the plan" },
729
- delete: { one: "Deleted a file", many: "Deleted %n files" },
730
- move: { one: "Moved a file", many: "Moved %n files" },
731
- think: { one: "Thought it through", many: "Thought it through" },
732
- image: { one: "Created an image", many: "Created %n images" },
733
- delegate: { one: "Delegated a task", many: "Delegated %n tasks" },
734
- collect: { one: "Collected a result", many: "Collected %n results" },
735
- status: { one: "Checked how it was going", many: "Checked %n times how it was going" },
736
- stop: { one: "Stopped a task", many: "Stopped %n tasks" },
737
- guide: { one: "Read a guide", many: "Read %n guides" },
738
- deliver: { one: "Delivered a file", many: "Delivered %n files" },
739
- sheet: { one: "Checked a sheet", many: "Checked %n sheets" },
740
- view: { one: "Built a view", many: "Built %n views" }
741
- };
742
-
743
635
  export {
744
636
  applyJsonPatch,
745
637
  createPilotState,
@@ -763,14 +655,7 @@ export {
763
655
  isNarration,
764
656
  splitNarration,
765
657
  narrationToMarkdown,
658
+ extractBlocks,
766
659
  extractReplyOptions,
767
- cleanReplyOption,
768
- isRunning,
769
- toolNames,
770
- toolName,
771
- stepLabel,
772
- summarizeTools,
773
- groupSteps,
774
- TOOL_VERBS_ES,
775
- TOOL_VERBS_EN
660
+ cleanReplyOption
776
661
  };
@@ -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") ?? "");